From 49c75de5fc139144b152207ba05f2936a2d25904 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Jun 2017 21:13:01 -0400 Subject: rewrite content interception using latest proposed API (#255) --- src/StardewModdingAPI/Framework/SGame.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SGame.cs') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 602a522b..e4c2a233 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -29,6 +30,9 @@ namespace StardewModdingAPI.Framework /**** ** SMAPI state ****/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -48,8 +52,6 @@ namespace StardewModdingAPI.Framework /// Whether the game's zoom level is at 100% (i.e. nothing should be scaled). public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f); - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; /**** ** Game state @@ -189,7 +191,7 @@ namespace StardewModdingAPI.Framework // 2. Since Game1.content isn't initialised yet, and we need one main instance to // support custom map tilesheets, detect when Game1.content is being initialised // and use the same instance. - this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); + this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); } /**** @@ -206,7 +208,7 @@ namespace StardewModdingAPI.Framework return mainContentManager; // build new instance - return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); + return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); } /// The method called when the game is updating its state. This happens roughly 60 times per second. -- cgit From 4568f2259ba6a0808658229122daa6ff6335a4fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 19:35:24 -0400 Subject: ensure there's only one content manager instance (#255) --- src/StardewModdingAPI/Framework/SGame.cs | 38 +++++++++++++++++++------------- 1 file changed, 23 insertions(+), 15 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SGame.cs') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index e4c2a233..80ae20ac 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// SMAPI's content manager. + private SContentManager SContentManager; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -177,21 +180,20 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. internal SGame(IMonitor monitor, IReflectionHelper reflection) { + // initialise this.Monitor = monitor; this.FirstUpdate = true; SGame.Instance = this; SGame.Reflection = reflection; - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley + // set XNA option required by Stardew Valley + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - // The game uses the default content manager instead of Game1.CreateContentManager in - // several cases (See http://community.playstarbound.com/threads/130058/page-27#post-3159274). - // The workaround is... - // 1. Override the default content manager. - // 2. Since Game1.content isn't initialised yet, and we need one main instance to - // support custom map tilesheets, detect when Game1.content is being initialised - // and use the same instance. - this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + // override content manager + this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); + this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + this.Content = this.SContentManager; + Game1.content = this.SContentManager; } /**** @@ -202,13 +204,19 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // When Game1.content is being initialised, use SMAPI's main content manager instance. - // See comment in SGame constructor. - if (Game1.content == null && this.Content is SContentManager mainContentManager) - return mainContentManager; + // return default if SMAPI's content manager isn't initialised yet + if (this.SContentManager == null) + { + this.Monitor?.Log("SMAPI's content manager isn't initialised; skipping content manager interception.", LogLevel.Trace); + return base.CreateContentManager(serviceProvider, rootDirectory); + } - // build new instance - return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + // return single instance if valid + if (serviceProvider != this.Content.ServiceProvider) + throw new InvalidOperationException("SMAPI uses a single content manager internally. You can't get a new content manager with a different service provider."); + if (rootDirectory != this.Content.RootDirectory) + throw new InvalidOperationException($"SMAPI uses a single content manager internally. You can't get a new content manager with a different root directory (current is {this.Content.RootDirectory}, requested {rootDirectory})."); + return this.SContentManager; } /// The method called when the game is updating its state. This happens roughly 60 times per second. -- cgit From 698328c52f60e6f825086585ef79f8c6eedb944e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Jul 2017 19:42:05 -0400 Subject: fix rare crash for some players when window loses focus (#306) --- release-notes.md | 1 + src/StardewModdingAPI/Framework/SGame.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SGame.cs') diff --git a/release-notes.md b/release-notes.md index ae2f853d..b1917ef7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -22,6 +22,7 @@ For players: * `list_items` now also matches translated item names when playing in another language. * `list_item_types` is a new command to see a list of item types. * Added clearer error when a `config.json` is invalid. +* Fixed rare crash when window loses focus for a few players (further to fix in 1.14). For modders: * You can now specify minimum dependency versions in `manifest.json`. diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 80ae20ac..39713d4a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -344,9 +344,21 @@ namespace StardewModdingAPI.Framework if (Game1.game1.IsActive) { // get latest state - KeyboardState keyState = Keyboard.GetState(); - MouseState mouseState = Mouse.GetState(); - Point mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); + KeyboardState keyState; + MouseState mouseState; + Point mousePosition; + try + { + keyState = Keyboard.GetState(); + mouseState = Mouse.GetState(); + mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); + } + catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + { + keyState = this.PreviousKeyState; + mouseState = this.PreviousMouseState; + mousePosition = this.PreviousMousePosition; + } // analyse state Keys[] currentlyPressedKeys = keyState.GetPressedKeys(); -- cgit From 771263299cae11d464c25c5291e59507c639e822 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 01:03:13 -0400 Subject: add SMAPI 2.0 compile mode --- README.md | 2 +- release-notes.md | 1 + .../Finders/PropertyFinder.cs | 83 ++++++++++++++++++++++ .../StardewModdingAPI.AssemblyRewriters.csproj | 1 + src/StardewModdingAPI/Command.cs | 4 +- src/StardewModdingAPI/Config.cs | 4 +- src/StardewModdingAPI/Constants.cs | 33 ++++++++- src/StardewModdingAPI/Events/EventArgsCommand.cs | 4 +- .../Events/EventArgsFarmerChanged.cs | 2 + .../Events/EventArgsLoadedGameChanged.cs | 4 +- src/StardewModdingAPI/Events/EventArgsNewDay.cs | 4 +- .../Events/EventArgsStringChanged.cs | 4 +- src/StardewModdingAPI/Events/GameEvents.cs | 22 ++++-- src/StardewModdingAPI/Events/PlayerEvents.cs | 6 ++ src/StardewModdingAPI/Events/TimeEvents.cs | 11 ++- .../Framework/ModLoading/ModResolver.cs | 22 +++++- src/StardewModdingAPI/Framework/Models/Manifest.cs | 2 + src/StardewModdingAPI/Framework/Monitor.cs | 2 + src/StardewModdingAPI/Framework/SContentManager.cs | 2 +- src/StardewModdingAPI/Framework/SGame.cs | 24 +++++-- src/StardewModdingAPI/Log.cs | 4 +- src/StardewModdingAPI/Mod.cs | 9 +++ src/StardewModdingAPI/Program.cs | 28 ++++++-- 23 files changed, 254 insertions(+), 24 deletions(-) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs (limited to 'src/StardewModdingAPI/Framework/SGame.cs') diff --git a/README.md b/README.md index 395cd314..ca3128e4 100644 --- a/README.md +++ b/README.md @@ -167,4 +167,4 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. - +`SMAPI_2_0` | Sets SMAPI 2.0 mode, which enables features planned for SMAPI 2.0 and removes all deprecated code. This helps test how mods will work when SMAPI 2.0 is released. diff --git a/release-notes.md b/release-notes.md index 7a7045fc..94106ba6 100644 --- a/release-notes.md +++ b/release-notes.md @@ -36,6 +36,7 @@ For modders: * Fixed corrupted state exceptions not being logged by SMAPI. For SMAPI developers: +* Added SMAPI 2.0 compile mode, for testing how mods will work with SMAPI 2.0. * Added prototype SMAPI 2.0 feature to override XNB files (not enabled for mods yet). * Added prototype SMAPI 2.0 support for version strings in `manifest.json` (not recommended for mods yet). diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs new file mode 100644 index 00000000..441f15f2 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs @@ -0,0 +1,83 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.AssemblyRewriters.Finders +{ + /// Finds incompatible CIL instructions that reference a given property and throws an . + public class PropertyFinder : IInstructionRewriter + { + /********* + ** Properties + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The property name for which to find references. + private readonly string PropertyName; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The property name for which to find references. + /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). + public PropertyFinder(string fullTypeName, string propertyName, string nounPhrase = null) + { + this.FullTypeName = fullTypeName; + this.PropertyName = propertyName; + this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{propertyName} property"; + } + + /// Rewrite a method definition for compatibility. + /// The module being rewritten. + /// The method definition to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return false; + } + + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return false; + + throw new IncompatibleInstructionException(this.NounPhrase); + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && (methodRef.Name == "get_" + this.PropertyName || methodRef.Name == "set_" + this.PropertyName); + } + } +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index e25b201e..7a12a8e9 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -49,6 +49,7 @@ Properties\GlobalAssemblyInfo.cs + diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs index e2d08538..7613b240 100644 --- a/src/StardewModdingAPI/Command.cs +++ b/src/StardewModdingAPI/Command.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; using System.Collections.Generic; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; @@ -155,3 +156,4 @@ namespace StardewModdingAPI } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs index 9f4bfad2..f6fe37d9 100644 --- a/src/StardewModdingAPI/Config.cs +++ b/src/StardewModdingAPI/Config.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; using System.IO; using System.Linq; using Newtonsoft.Json; @@ -184,3 +185,4 @@ namespace StardewModdingAPI } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index bd489b29..06a8c486 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -33,7 +33,12 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 14, 1); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} + public static ISemanticVersion ApiVersion { get; } = +#if SMAPI_2_0 + new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); +#else + new SemanticVersion(1, 15, 0, "prerelease.1"); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} +#endif /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); @@ -169,6 +174,32 @@ namespace StardewModdingAPI new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), + // APIs removed in SMAPI 2.0 +#if SMAPI_2_0 + new TypeFinder("StardewModdingAPI.Command"), + new TypeFinder("StardewModdingAPI.Config"), + new TypeFinder("StardewModdingAPI.Log"), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), +#endif + /**** ** Rewriters change CIL as needed to fix incompatible code ****/ diff --git a/src/StardewModdingAPI/Events/EventArgsCommand.cs b/src/StardewModdingAPI/Events/EventArgsCommand.cs index 88a9e5a3..f0435904 100644 --- a/src/StardewModdingAPI/Events/EventArgsCommand.cs +++ b/src/StardewModdingAPI/Events/EventArgsCommand.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -24,3 +25,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs index 699d90be..c34fc4ab 100644 --- a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs @@ -1,3 +1,4 @@ +#if !SMAPI_2_0 using System; using SFarmer = StardewValley.Farmer; @@ -29,3 +30,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs b/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs index 51d64016..d6fc4594 100644 --- a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -23,3 +24,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsNewDay.cs b/src/StardewModdingAPI/Events/EventArgsNewDay.cs index aba837e4..5bd2ba66 100644 --- a/src/StardewModdingAPI/Events/EventArgsNewDay.cs +++ b/src/StardewModdingAPI/Events/EventArgsNewDay.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -33,3 +34,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs b/src/StardewModdingAPI/Events/EventArgsStringChanged.cs index 85b6fab5..1498cb71 100644 --- a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsStringChanged.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -27,3 +28,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 8e3cf662..c97b2c36 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; @@ -29,6 +30,7 @@ namespace StardewModdingAPI.Events /// The backing field for . [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _FirstUpdateTick; +#endif /********* @@ -40,6 +42,7 @@ namespace StardewModdingAPI.Events /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. internal static event EventHandler GameLoadedInternal; +#if !SMAPI_2_0 /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] public static event EventHandler Initialize @@ -87,6 +90,7 @@ namespace StardewModdingAPI.Events } remove => GameEvents._FirstUpdateTick -= value; } +#endif /// Raised when the game updates its state (≈60 times per second). public static event EventHandler UpdateTick; @@ -113,42 +117,52 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) { GameEvents.DeprecationManager = deprecationManager; } +#endif - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeInitialize(IMonitor monitor) + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeInitialize(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); +#if !SMAPI_2_0 monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", GameEvents._Initialize?.GetInvocationList()); +#endif } +#if !SMAPI_2_0 /// Raise a event. /// Encapsulates logging and monitoring. internal static void InvokeLoadContent(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); } +#endif - /// Raise a event. + /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeGameLoaded(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList()); +#if !SMAPI_2_0 monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); +#endif } +#if !SMAPI_2_0 /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeFirstUpdateTick(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents._FirstUpdateTick?.GetInvocationList()); } +#endif /// Raise an event. /// Encapsulates logging and monitoring. diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index 37649fee..efada876 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -15,6 +15,7 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; @@ -25,11 +26,13 @@ namespace StardewModdingAPI.Events /// The backing field for . [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _FarmerChanged; +#endif /********* ** Events *********/ +#if !SMAPI_2_0 /// Raised after the player loads a saved game. [Obsolete("Use " + nameof(SaveEvents) + "." + nameof(SaveEvents.AfterLoad) + " instead")] public static event EventHandler LoadedGame @@ -53,6 +56,7 @@ namespace StardewModdingAPI.Events } remove => PlayerEvents._FarmerChanged -= value; } +#endif /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). public static event EventHandler InventoryChanged; @@ -64,6 +68,7 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) @@ -87,6 +92,7 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", PlayerEvents._FarmerChanged?.GetInvocationList(), null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); } +#endif /// Raise an event. /// Encapsulates monitoring and logging. diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index 5dadf567..520f8b24 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; @@ -29,6 +30,8 @@ namespace StardewModdingAPI.Events /// The backing field for . [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _YearOfGameChanged; +#endif + /********* ** Events @@ -39,6 +42,7 @@ namespace StardewModdingAPI.Events /// Raised after the in-game clock changes. public static event EventHandler TimeOfDayChanged; +#if !SMAPI_2_0 /// Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use instead. [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] public static event EventHandler DayOfMonthChanged @@ -86,17 +90,20 @@ namespace StardewModdingAPI.Events } remove => TimeEvents._OnNewDay -= value; } +#endif /********* ** Internal methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) { TimeEvents.DeprecationManager = deprecationManager; } +#endif /// Raise an event. /// Encapsulates monitoring and logging. @@ -105,7 +112,7 @@ namespace StardewModdingAPI.Events monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); } - /// Raise a event. + /// Raise a event. /// Encapsulates monitoring and logging. /// The previous time in military time format (e.g. 6:00pm is 1800). /// The current time in military time format (e.g. 6:10pm is 1810). @@ -114,6 +121,7 @@ namespace StardewModdingAPI.Events monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); } +#if !SMAPI_2_0 /// Raise a event. /// Encapsulates monitoring and logging. /// The previous day value. @@ -150,5 +158,6 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", TimeEvents._OnNewDay?.GetInvocationList(), null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); } +#endif } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index cefc860b..ceb51bbb 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -109,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModLoading bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl); bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl); - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; + string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:"; if (hasOfficialUrl) error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; @@ -131,7 +131,27 @@ namespace StardewModdingAPI.Framework.ModLoading // validate DLL path string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) + { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + + // validate required fields +#if SMAPI_2_0 + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + } +#endif } } diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 8e5d13f8..08b88025 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -38,9 +38,11 @@ namespace StardewModdingAPI.Framework.Models /// The unique mod ID. public string UniqueID { get; set; } +#if !SMAPI_2_0 /// Whether the mod uses per-save config files. [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] public bool PerSaveConfigs { get; set; } +#endif /// Any manifest fields which didn't match a valid field. [JsonExtensionData] diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 7d40b72b..64cc0bdc 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -104,6 +104,7 @@ namespace StardewModdingAPI.Framework this.LogFile.WriteLine(""); } +#if !SMAPI_2_0 /// Log a message for the player or developer, using the specified console color. /// The name of the mod logging the message. /// The message to log. @@ -114,6 +115,7 @@ namespace StardewModdingAPI.Framework { this.LogImpl(source, message, level, color); } +#endif /********* diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0a8a0873..5707aab1 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -206,7 +206,7 @@ namespace StardewModdingAPI.Framework // from Farmer constructor if (Game1.player != null) - Game1.player.FarmerRenderer = new FarmerRenderer(this.Load($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + Game1.player.FarmerRenderer = new FarmerRenderer(this.Load("Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 39713d4a..678dcf3a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Framework private readonly IMonitor Monitor; /// SMAPI's content manager. - private SContentManager SContentManager; + private readonly SContentManager SContentManager; /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -113,6 +113,7 @@ namespace StardewModdingAPI.Framework /// The time of day (in 24-hour military format) at last check. private int PreviousTime; +#if !SMAPI_2_0 /// The day of month (1–28) at last check. private int PreviousDay; @@ -127,6 +128,7 @@ namespace StardewModdingAPI.Framework /// The player character at last check. private SFarmer PreviousFarmer; +#endif /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -285,7 +287,9 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) { GameEvents.InvokeInitialize(this.Monitor); +#if !SMAPI_2_0 GameEvents.InvokeLoadContent(this.Monitor); +#endif GameEvents.InvokeGameLoaded(this.Monitor); } @@ -315,7 +319,9 @@ namespace StardewModdingAPI.Framework Context.IsWorldReady = true; SaveEvents.InvokeAfterLoad(this.Monitor); +#if !SMAPI_2_0 PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); +#endif TimeEvents.InvokeAfterDayStarted(this.Monitor); } this.AfterLoadTimer--; @@ -460,9 +466,11 @@ namespace StardewModdingAPI.Framework if (this.GetHash(Game1.locations) != this.PreviousGameLocations) LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); +#if !SMAPI_2_0 // raise player changed if (Game1.player != this.PreviousFarmer) PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); +#endif // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) @@ -493,12 +501,14 @@ namespace StardewModdingAPI.Framework // raise time changed if (Game1.timeOfDay != this.PreviousTime) TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); +#if !SMAPI_2_0 if (Game1.dayOfMonth != this.PreviousDay) TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); if (Game1.currentSeason != this.PreviousSeason) TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); if (Game1.year != this.PreviousYear) TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); +#endif // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) @@ -508,7 +518,6 @@ namespace StardewModdingAPI.Framework // update state this.PreviousGameLocations = this.GetHash(Game1.locations); this.PreviousGameLocation = Game1.currentLocation; - this.PreviousFarmer = Game1.player; this.PreviousCombatLevel = Game1.player.combatLevel; this.PreviousFarmingLevel = Game1.player.farmingLevel; this.PreviousFishingLevel = Game1.player.fishingLevel; @@ -518,21 +527,26 @@ namespace StardewModdingAPI.Framework this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); this.PreviousTime = Game1.timeOfDay; + this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; + this.PreviousSaveID = Game1.uniqueIDForThisGame; +#if !SMAPI_2_0 + this.PreviousFarmer = Game1.player; this.PreviousDay = Game1.dayOfMonth; this.PreviousSeason = Game1.currentSeason; this.PreviousYear = Game1.year; - this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; - this.PreviousSaveID = Game1.uniqueIDForThisGame; +#endif } /********* ** Game day transition event (obsolete) *********/ +#if !SMAPI_2_0 if (Game1.newDay != this.PreviousIsNewDay) { TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); this.PreviousIsNewDay = Game1.newDay; } +#endif /********* ** Game update @@ -552,7 +566,9 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeUpdateTick(this.Monitor); if (this.FirstUpdate) { +#if !SMAPI_2_0 GameEvents.InvokeFirstUpdateTick(this.Monitor); +#endif this.FirstUpdate = false; } if (this.CurrentUpdateTick % 2 == 0) diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs index d58cebfe..562fa1f8 100644 --- a/src/StardewModdingAPI/Log.cs +++ b/src/StardewModdingAPI/Log.cs @@ -1,3 +1,4 @@ +#if !SMAPI_2_0 using System; using System.Threading; using StardewModdingAPI.Framework; @@ -315,4 +316,5 @@ namespace StardewModdingAPI return Log.ModRegistry.GetModFromStack() ?? ""; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index 171088cf..302f16ec 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -11,11 +11,14 @@ namespace StardewModdingAPI /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; + /// The backing field for . private string _pathOnDisk; +#endif /********* @@ -30,6 +33,7 @@ namespace StardewModdingAPI /// The mod's manifest. public IManifest ModManifest { get; internal set; } +#if !SMAPI_2_0 /// The full path to the mod's directory on the disk. [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.DirectoryPath) + " instead")] public string PathOnDisk @@ -69,11 +73,13 @@ namespace StardewModdingAPI return Context.IsSaveLoaded ? Path.Combine(this.PerSaveConfigFolder, $"{Constants.SaveFolderName}.json") : ""; } } +#endif /********* ** Public methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) @@ -84,6 +90,7 @@ namespace StardewModdingAPI /// The mod entry point, called after the mod is first loaded. [Obsolete("This overload is obsolete since SMAPI 1.0.")] public virtual void Entry(params object[] objects) { } +#endif /// The mod entry point, called after the mod is first loaded. /// Provides simplified APIs for writing mods. @@ -101,6 +108,7 @@ namespace StardewModdingAPI /********* ** Private methods *********/ +#if !SMAPI_2_0 /// Get the full path to the per-save configuration file for the current save (if is true). [Obsolete] private string GetPerSaveConfigFolder() @@ -115,6 +123,7 @@ namespace StardewModdingAPI } return Path.Combine(this.PathOnDisk, "psconfigs"); } +#endif /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index e960a684..d722b43e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -226,6 +226,7 @@ namespace StardewModdingAPI } } +#if !SMAPI_2_0 /// Get a monitor for legacy code which doesn't have one passed in. [Obsolete("This method should only be used when needed for backwards compatibility.")] internal IMonitor GetLegacyMonitorForMod() @@ -233,6 +234,7 @@ namespace StardewModdingAPI string modName = this.ModRegistry.GetModFromStack() ?? "unknown"; return this.GetSecondaryMonitor(modName); } +#endif /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() @@ -323,6 +325,7 @@ namespace StardewModdingAPI this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); +#if !SMAPI_2_0 // inject compatibility shims #pragma warning disable 618 Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); @@ -333,6 +336,7 @@ namespace StardewModdingAPI PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); #pragma warning restore 618 +#endif // redirect direct console output { @@ -369,6 +373,7 @@ namespace StardewModdingAPI resolver.ValidateManifests(mods, Constants.ApiVersion); // check for deprecated metadata +#if !SMAPI_2_0 IList deprecationWarnings = new List(); foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) { @@ -403,15 +408,20 @@ namespace StardewModdingAPI mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); } } - } + } +#endif // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); // load mods +#if SMAPI_2_0 + this.LoadMods(mods, new JsonHelper(), this.ContentManager); +#else this.LoadMods(mods, new JsonHelper(), this.ContentManager, deprecationWarnings); foreach (Action warning in deprecationWarnings) warning(); +#endif } if (this.Monitor.IsExiting) { @@ -576,8 +586,12 @@ namespace StardewModdingAPI /// The mods to load. /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. - /// A list to populate with any deprecation warnings. +#if !SMAPI_2_0 +/// A list to populate with any deprecation warnings. private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) +#else + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) +#endif { this.Monitor.Log("Loading mods...", LogLevel.Trace); @@ -615,7 +629,7 @@ namespace StardewModdingAPI } catch (IncompatibleInstructionException ex) { - TrackSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version})."); + TrackSkip(metadata, $"it's not compatible with the latest version of the game or SMAPI (detected {ex.NounPhrase}). Please check for a newer version of the mod."); continue; } catch (Exception ex) @@ -657,6 +671,7 @@ namespace StardewModdingAPI continue; } +#if !SMAPI_2_0 // prevent mods from using SMAPI 2.0 content interception before release // ReSharper disable SuspiciousTypeConversion.Global if (mod is IAssetEditor || mod is IAssetLoader) @@ -664,12 +679,15 @@ namespace StardewModdingAPI TrackSkip(metadata, $"its entry class implements {nameof(IAssetEditor)} or {nameof(IAssetLoader)}. These are part of a prototype API that isn't available for mods to use yet."); } // ReSharper restore SuspiciousTypeConversion.Global +#endif // inject data mod.ModManifest = manifest; mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); +#if !SMAPI_2_0 mod.PathOnDisk = metadata.DirectoryPath; +#endif // track mod metadata.SetMod(mod); @@ -729,12 +747,14 @@ namespace StardewModdingAPI try { IMod mod = metadata.Mod; - (mod as Mod)?.Entry(); // deprecated since 1.0 mod.Entry(mod.Helper); +#if !SMAPI_2_0 + (mod as Mod)?.Entry(); // deprecated since 1.0 // raise deprecation warning for old Entry() methods if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info)); +#endif } catch (Exception ex) { -- cgit From 8d301162d87558826ed8fc8f2352800bf674ddf0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 5 Jul 2017 15:41:58 -0400 Subject: add InputEvents which unify keyboard, mouse, and controller input with more metadata (#316) --- release-notes.md | 6 +- src/StardewModdingAPI/Events/EventArgsInput.cs | 38 ++ src/StardewModdingAPI/Events/InputEvents.cs | 45 ++ src/StardewModdingAPI/Framework/CursorPosition.cs | 37 ++ src/StardewModdingAPI/Framework/SGame.cs | 268 ++++----- src/StardewModdingAPI/ICursorPosition.cs | 19 + src/StardewModdingAPI/StardewModdingAPI.csproj | 5 + src/StardewModdingAPI/Utilities/SButton.cs | 659 ++++++++++++++++++++++ 8 files changed, 932 insertions(+), 145 deletions(-) create mode 100644 src/StardewModdingAPI/Events/EventArgsInput.cs create mode 100644 src/StardewModdingAPI/Events/InputEvents.cs create mode 100644 src/StardewModdingAPI/Framework/CursorPosition.cs create mode 100644 src/StardewModdingAPI/ICursorPosition.cs create mode 100644 src/StardewModdingAPI/Utilities/SButton.cs (limited to 'src/StardewModdingAPI/Framework/SGame.cs') diff --git a/release-notes.md b/release-notes.md index 2283d6d1..5d906f6c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,8 +6,10 @@ For players: * The SMAPI console is now much simpler and easier-to-read. For mod developers: -* SMAPI mods can now edit XNB images & data loaded by the game (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). -* SMAPI mods can now inject new XNB images & data (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* Added API to edit XNB images & data loaded by the game (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* Added API to inject new XNB images & data (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* Added `InputEvents` which unify keyboard, mouse, and controller input for much simpler input handling (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Input_events)). +* Added useful `InputEvents` metadata like the cursor position, grab tile, etc. * The `manifest.json` version can now be specified as a string. ## 1.15 diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/StardewModdingAPI/Events/EventArgsInput.cs new file mode 100644 index 00000000..1d5e6fde --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsInput.cs @@ -0,0 +1,38 @@ +#if SMAPI_2_0 +using System; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed or released. + public class EventArgsInput : EventArgs + { + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; set; } + + /// Whether the input is considered a 'click' by the game for enabling action. + public bool IsClick { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) + { + this.Button = button; + this.Cursor = cursor; + this.IsClick = isClick; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/StardewModdingAPI/Events/InputEvents.cs new file mode 100644 index 00000000..285487af --- /dev/null +++ b/src/StardewModdingAPI/Events/InputEvents.cs @@ -0,0 +1,45 @@ +#if SMAPI_2_0 +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player uses a controller, keyboard, or mouse button. + public static class InputEvents + { + /********* + ** Events + *********/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public static event EventHandler ButtonPressed; + + /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + public static event EventHandler ButtonReleased; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + { + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + { + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + } + } +} +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/StardewModdingAPI/Framework/CursorPosition.cs new file mode 100644 index 00000000..4f256da5 --- /dev/null +++ b/src/StardewModdingAPI/Framework/CursorPosition.cs @@ -0,0 +1,37 @@ +#if SMAPI_2_0 +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Framework +{ + /// Defines a position on a given map at different reference points. + internal class CursorPosition : ICursorPosition + { + /********* + ** Accessors + *********/ + /// The pixel position relative to the top-left corner of the visible screen. + public Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + public Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + public Vector2 GrabTile { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The pixel position relative to the top-left corner of the visible screen. + /// The tile position relative to the top-left corner of the map. + /// The tile position that the game considers under the cursor for purposes of clicking actions. + public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + { + this.ScreenPixels = screenPixels; + this.Tile = tile; + this.GrabTile = grabTile; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 678dcf3a..f2c5c0c9 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -59,12 +60,15 @@ namespace StardewModdingAPI.Framework /**** ** Game state ****/ - /// Arrays of pressed controller buttons indexed by . - private Buttons[] PreviousPressedButtons = new Buttons[0]; + /// A record of the buttons pressed as of the previous tick. + private SButton[] PreviousPressedButtons = new SButton[0]; /// A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick. private KeyboardState PreviousKeyState; + /// A record of the controller state (i.e. the up/down state for each button) as of the previous tick. + private GamePadState PreviousControllerState; + /// A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick. private MouseState PreviousMouseState; @@ -351,64 +355,95 @@ namespace StardewModdingAPI.Framework { // get latest state KeyboardState keyState; + GamePadState controllerState; MouseState mouseState; Point mousePosition; try { keyState = Keyboard.GetState(); + controllerState = GamePad.GetState(PlayerIndex.One); mouseState = Mouse.GetState(); mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); } catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true { keyState = this.PreviousKeyState; + controllerState = this.PreviousControllerState; mouseState = this.PreviousMouseState; mousePosition = this.PreviousMousePosition; } // analyse state - Keys[] currentlyPressedKeys = keyState.GetPressedKeys(); - Keys[] previousPressedKeys = this.PreviousKeyState.GetPressedKeys(); - Keys[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); - Keys[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); - - // raise key pressed - foreach (Keys key in framePressedKeys) - ControlEvents.InvokeKeyPressed(this.Monitor, key); - - // raise key released - foreach (Keys key in frameReleasedKeys) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + SButton[] currentlyPressedKeys = this.GetPressedButtons(keyState, mouseState, controllerState).ToArray(); + SButton[] previousPressedKeys = this.PreviousPressedButtons; + SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); + SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); + bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); + + // get cursor position +#if SMAPI_2_0 + ICursorPosition cursor; + { + // cursor position + Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); + Vector2 tile = new Vector2((Game1.viewport.X + screenPixels.X) / Game1.tileSize, (Game1.viewport.Y + screenPixels.Y) / Game1.tileSize); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); + } +#endif - // raise controller button pressed - foreach (Buttons button in this.GetFramePressedButtons()) + // raise button pressed + foreach (SButton button in framePressedKeys) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) +#if SMAPI_2_0 + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); +#endif + + // legacy events + if (button.TryGetKeyboard(out Keys key)) { - var triggers = GamePad.GetState(PlayerIndex.One).Triggers; - ControlEvents.InvokeTriggerPressed(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right); + if (key != Keys.None) + ControlEvents.InvokeKeyPressed(this.Monitor, key); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); + else + ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); } - else - ControlEvents.InvokeButtonPressed(this.Monitor, button); } - // raise controller button released - foreach (Buttons button in this.GetFrameReleasedButtons()) + // raise button released + foreach (SButton button in frameReleasedKeys) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) +#if SMAPI_2_0 + bool wasClick = + (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click + || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); +#endif + + // legacy events + if (button.TryGetKeyboard(out Keys key)) { - var triggers = GamePad.GetState(PlayerIndex.One).Triggers; - ControlEvents.InvokeTriggerReleased(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right); + if (key != Keys.None) + ControlEvents.InvokeKeyReleased(this.Monitor, key); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); + else + ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); } - else - ControlEvents.InvokeButtonReleased(this.Monitor, button); } - // raise keyboard state changed + // raise legacy state-changed events if (keyState != this.PreviousKeyState) ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousKeyState, keyState); - - // raise mouse state changed if (mouseState != this.PreviousMouseState) ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousMouseState, mouseState, this.PreviousMousePosition, mousePosition); @@ -416,7 +451,8 @@ namespace StardewModdingAPI.Framework this.PreviousMouseState = mouseState; this.PreviousMousePosition = mousePosition; this.PreviousKeyState = keyState; - this.PreviousPressedButtons = this.GetButtonsDown(); + this.PreviousControllerState = controllerState; + this.PreviousPressedButtons = currentlyPressedKeys; } /********* @@ -1308,120 +1344,66 @@ namespace StardewModdingAPI.Framework this.PreviousSaveID = 0; } - /// Get the controller buttons which are currently pressed. - private Buttons[] GetButtonsDown() + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) { - var state = GamePad.GetState(PlayerIndex.One); - var buttons = new List(); - if (state.IsConnected) + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) { - if (state.Buttons.A == ButtonState.Pressed) buttons.Add(Buttons.A); - if (state.Buttons.B == ButtonState.Pressed) buttons.Add(Buttons.B); - if (state.Buttons.Back == ButtonState.Pressed) buttons.Add(Buttons.Back); - if (state.Buttons.BigButton == ButtonState.Pressed) buttons.Add(Buttons.BigButton); - if (state.Buttons.LeftShoulder == ButtonState.Pressed) buttons.Add(Buttons.LeftShoulder); - if (state.Buttons.LeftStick == ButtonState.Pressed) buttons.Add(Buttons.LeftStick); - if (state.Buttons.RightShoulder == ButtonState.Pressed) buttons.Add(Buttons.RightShoulder); - if (state.Buttons.RightStick == ButtonState.Pressed) buttons.Add(Buttons.RightStick); - if (state.Buttons.Start == ButtonState.Pressed) buttons.Add(Buttons.Start); - if (state.Buttons.X == ButtonState.Pressed) buttons.Add(Buttons.X); - if (state.Buttons.Y == ButtonState.Pressed) buttons.Add(Buttons.Y); - if (state.DPad.Up == ButtonState.Pressed) buttons.Add(Buttons.DPadUp); - if (state.DPad.Down == ButtonState.Pressed) buttons.Add(Buttons.DPadDown); - if (state.DPad.Left == ButtonState.Pressed) buttons.Add(Buttons.DPadLeft); - if (state.DPad.Right == ButtonState.Pressed) buttons.Add(Buttons.DPadRight); - if (state.Triggers.Left > 0.2f) buttons.Add(Buttons.LeftTrigger); - if (state.Triggers.Right > 0.2f) buttons.Add(Buttons.RightTrigger); + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; } - return buttons.ToArray(); - } - - /// Get the controller buttons which were pressed after the last update. - private Buttons[] GetFramePressedButtons() - { - var state = GamePad.GetState(PlayerIndex.One); - var buttons = new List(); - if (state.IsConnected) - { - if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A); - if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B); - if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back); - if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton); - if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder); - if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick); - if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder); - if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick); - if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start); - if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X); - if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y); - if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp); - if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown); - if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft); - if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight); - if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger); - if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger); - } - return buttons.ToArray(); - } - - /// Get the controller buttons which were released after the last update. - private Buttons[] GetFrameReleasedButtons() - { - var state = GamePad.GetState(PlayerIndex.One); - var buttons = new List(); - if (state.IsConnected) - { - if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A); - if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B); - if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back); - if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton); - if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder); - if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick); - if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder); - if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick); - if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start); - if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X); - if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y); - if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp); - if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown); - if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft); - if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight); - if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger); - if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger); - } - return buttons.ToArray(); - } - - /// Get whether a controller button was pressed since the last check. - /// The controller button to check. - /// The last known state. - private bool WasButtonJustPressed(Buttons button, ButtonState buttonState) - { - return buttonState == ButtonState.Pressed && !this.PreviousPressedButtons.Contains(button); - } - - /// Get whether a controller button was released since the last check. - /// The controller button to check. - /// The last known state. - private bool WasButtonJustReleased(Buttons button, ButtonState buttonState) - { - return buttonState == ButtonState.Released && this.PreviousPressedButtons.Contains(button); - } - - /// Get whether an analogue controller button was pressed since the last check. - /// The controller button to check. - /// The last known value. - private bool WasButtonJustPressed(Buttons button, float value) - { - return this.WasButtonJustPressed(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released); - } - - /// Get whether an analogue controller button was released since the last check. - /// The controller button to check. - /// The last known value. - private bool WasButtonJustReleased(Buttons button, float value) - { - return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released); } /// Get the player inventory changes between two states. diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/StardewModdingAPI/ICursorPosition.cs new file mode 100644 index 00000000..d03cda71 --- /dev/null +++ b/src/StardewModdingAPI/ICursorPosition.cs @@ -0,0 +1,19 @@ +#if SMAPI_2_0 +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI +{ + /// Represents a cursor position in the different coordinate systems. + public interface ICursorPosition + { + /// The pixel position relative to the top-left corner of the visible screen. + Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + Vector2 GrabTile { get; } + } +} +#endif diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index bf1c43d1..c442cc8a 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -93,7 +93,9 @@ + + @@ -207,7 +209,10 @@ + + + diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/StardewModdingAPI/Utilities/SButton.cs new file mode 100644 index 00000000..f4fccfff --- /dev/null +++ b/src/StardewModdingAPI/Utilities/SButton.cs @@ -0,0 +1,659 @@ +using System; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Utilities +{ + /// A unified button constant which includes all controller, keyboard, and mouse buttons. + /// Derived from , , and . +#if SMAPI_2_0 + public +#else + internal +#endif + enum SButton + { + /// No valid key. + None = 0, + + /********* + ** Mouse + *********/ + /// The left mouse button. + MouseLeft = 1000, + + /// The right mouse button. + MouseRight = 1001, + + /// The middle mouse button. + MouseMiddle = 1002, + + /// The first mouse XButton. + MouseX1 = 1003, + + /// The second mouse XButton. + MouseX2 = 1004, + + /********* + ** Controller + *********/ + /// The 'A' button on a controller. + ControllerA = SButtonExtensions.ControllerOffset + Buttons.A, + + /// The 'B' button on a controller. + ControllerB = SButtonExtensions.ControllerOffset + Buttons.B, + + /// The 'X' button on a controller. + ControllerX = SButtonExtensions.ControllerOffset + Buttons.X, + + /// The 'Y' button on a controller. + ControllerY = SButtonExtensions.ControllerOffset + Buttons.Y, + + /// The back button on a controller. + ControllerBack = SButtonExtensions.ControllerOffset + Buttons.Back, + + /// The start button on a controller. + ControllerStart = SButtonExtensions.ControllerOffset + Buttons.Start, + + /// The up button on the directional pad of a controller. + DPadUp = SButtonExtensions.ControllerOffset + Buttons.DPadUp, + + /// The down button on the directional pad of a controller. + DPadDown = SButtonExtensions.ControllerOffset + Buttons.DPadDown, + + /// The left button on the directional pad of a controller. + DPadLeft = SButtonExtensions.ControllerOffset + Buttons.DPadLeft, + + /// The right button on the directional pad of a controller. + DPadRight = SButtonExtensions.ControllerOffset + Buttons.DPadRight, + + /// The left bumper (shoulder) button on a controller. + LeftShoulder = SButtonExtensions.ControllerOffset + Buttons.LeftShoulder, + + /// The right bumper (shoulder) button on a controller. + RightShoulder = SButtonExtensions.ControllerOffset + Buttons.RightShoulder, + + /// The left trigger on a controller. + LeftTrigger = SButtonExtensions.ControllerOffset + Buttons.LeftTrigger, + + /// The right trigger on a controller. + RightTrigger = SButtonExtensions.ControllerOffset + Buttons.RightTrigger, + + /// The left analog stick on a controller (when pressed). + LeftStick = SButtonExtensions.ControllerOffset + Buttons.LeftStick, + + /// The right analog stick on a controller (when pressed). + RightStick = SButtonExtensions.ControllerOffset + Buttons.RightStick, + + /// The 'big button' on a controller. + BigButton = SButtonExtensions.ControllerOffset + Buttons.BigButton, + + /// The left analog stick on a controller (when pushed left). + LeftThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickLeft, + + /// The left analog stick on a controller (when pushed right). + LeftThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickRight, + + /// The left analog stick on a controller (when pushed down). + LeftThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickDown, + + /// The left analog stick on a controller (when pushed up). + LeftThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickUp, + + /// The right analog stick on a controller (when pushed left). + RightThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickLeft, + + /// The right analog stick on a controller (when pushed right). + RightThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickRight, + + /// The right analog stick on a controller (when pushed down). + RightThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickDown, + + /// The right analog stick on a controller (when pushed up). + RightThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickUp, + + /********* + ** Keyboard + *********/ + /// The A button on a keyboard. + A = Keys.A, + + /// The Add button on a keyboard. + Add = Keys.Add, + + /// The Applications button on a keyboard. + Apps = Keys.Apps, + + /// The Attn button on a keyboard. + Attn = Keys.Attn, + + /// The B button on a keyboard. + B = Keys.B, + + /// The Backspace button on a keyboard. + Back = Keys.Back, + + /// The Browser Back button on a keyboard in Windows 2000/XP. + BrowserBack = Keys.BrowserBack, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserFavorites = Keys.BrowserFavorites, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserForward = Keys.BrowserForward, + + /// The Browser Home button on a keyboard in Windows 2000/XP. + BrowserHome = Keys.BrowserHome, + + /// The Browser Refresh button on a keyboard in Windows 2000/XP. + BrowserRefresh = Keys.BrowserRefresh, + + /// The Browser Search button on a keyboard in Windows 2000/XP. + BrowserSearch = Keys.BrowserSearch, + + /// The Browser Stop button on a keyboard in Windows 2000/XP. + BrowserStop = Keys.BrowserStop, + + /// The C button on a keyboard. + C = Keys.C, + + /// The Caps Lock button on a keyboard. + CapsLock = Keys.CapsLock, + + /// The Green ChatPad button on a keyboard. + ChatPadGreen = Keys.ChatPadGreen, + + /// The Orange ChatPad button on a keyboard. + ChatPadOrange = Keys.ChatPadOrange, + + /// The CrSel button on a keyboard. + Crsel = Keys.Crsel, + + /// The D button on a keyboard. + D = Keys.D, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D0 = Keys.D0, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D1 = Keys.D1, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D2 = Keys.D2, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D3 = Keys.D3, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D4 = Keys.D4, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D5 = Keys.D5, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D6 = Keys.D6, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D7 = Keys.D7, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D8 = Keys.D8, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D9 = Keys.D9, + + /// The Decimal button on a keyboard. + Decimal = Keys.Decimal, + + /// The Delete button on a keyboard. + Delete = Keys.Delete, + + /// The Divide button on a keyboard. + Divide = Keys.Divide, + + /// The Down arrow button on a keyboard. + Down = Keys.Down, + + /// The E button on a keyboard. + E = Keys.E, + + /// The End button on a keyboard. + End = Keys.End, + + /// The Enter button on a keyboard. + Enter = Keys.Enter, + + /// The Erase EOF button on a keyboard. + EraseEof = Keys.EraseEof, + + /// The Escape button on a keyboard. + Escape = Keys.Escape, + + /// The Execute button on a keyboard. + Execute = Keys.Execute, + + /// The ExSel button on a keyboard. + Exsel = Keys.Exsel, + + /// The F button on a keyboard. + F = Keys.F, + + /// The F1 button on a keyboard. + F1 = Keys.F1, + + /// The F10 button on a keyboard. + F10 = Keys.F10, + + /// The F11 button on a keyboard. + F11 = Keys.F11, + + /// The F12 button on a keyboard. + F12 = Keys.F12, + + /// The F13 button on a keyboard. + F13 = Keys.F13, + + /// The F14 button on a keyboard. + F14 = Keys.F14, + + /// The F15 button on a keyboard. + F15 = Keys.F15, + + /// The F16 button on a keyboard. + F16 = Keys.F16, + + /// The F17 button on a keyboard. + F17 = Keys.F17, + + /// The F18 button on a keyboard. + F18 = Keys.F18, + + /// The F19 button on a keyboard. + F19 = Keys.F19, + + /// The F2 button on a keyboard. + F2 = Keys.F2, + + /// The F20 button on a keyboard. + F20 = Keys.F20, + + /// The F21 button on a keyboard. + F21 = Keys.F21, + + /// The F22 button on a keyboard. + F22 = Keys.F22, + + /// The F23 button on a keyboard. + F23 = Keys.F23, + + /// The F24 button on a keyboard. + F24 = Keys.F24, + + /// The F3 button on a keyboard. + F3 = Keys.F3, + + /// The F4 button on a keyboard. + F4 = Keys.F4, + + /// The F5 button on a keyboard. + F5 = Keys.F5, + + /// The F6 button on a keyboard. + F6 = Keys.F6, + + /// The F7 button on a keyboard. + F7 = Keys.F7, + + /// The F8 button on a keyboard. + F8 = Keys.F8, + + /// The F9 button on a keyboard. + F9 = Keys.F9, + + /// The G button on a keyboard. + G = Keys.G, + + /// The H button on a keyboard. + H = Keys.H, + + /// The Help button on a keyboard. + Help = Keys.Help, + + /// The Home button on a keyboard. + Home = Keys.Home, + + /// The I button on a keyboard. + I = Keys.I, + + /// The IME Convert button on a keyboard. + ImeConvert = Keys.ImeConvert, + + /// The IME NoConvert button on a keyboard. + ImeNoConvert = Keys.ImeNoConvert, + + /// The INS button on a keyboard. + Insert = Keys.Insert, + + /// The J button on a keyboard. + J = Keys.J, + + /// The K button on a keyboard. + K = Keys.K, + + /// The Kana button on a Japanese keyboard. + Kana = Keys.Kana, + + /// The Kanji button on a Japanese keyboard. + Kanji = Keys.Kanji, + + /// The L button on a keyboard. + L = Keys.L, + + /// The Start Applications 1 button on a keyboard in Windows 2000/XP. + LaunchApplication1 = Keys.LaunchApplication1, + + /// The Start Applications 2 button on a keyboard in Windows 2000/XP. + LaunchApplication2 = Keys.LaunchApplication2, + + /// The Start Mail button on a keyboard in Windows 2000/XP. + LaunchMail = Keys.LaunchMail, + + /// The Left arrow button on a keyboard. + Left = Keys.Left, + + /// The Left Alt button on a keyboard. + LeftAlt = Keys.LeftAlt, + + /// The Left Control button on a keyboard. + LeftControl = Keys.LeftControl, + + /// The Left Shift button on a keyboard. + LeftShift = Keys.LeftShift, + + /// The Left Windows button on a keyboard. + LeftWindows = Keys.LeftWindows, + + /// The M button on a keyboard. + M = Keys.M, + + /// The MediaNextTrack button on a keyboard in Windows 2000/XP. + MediaNextTrack = Keys.MediaNextTrack, + + /// The MediaPlayPause button on a keyboard in Windows 2000/XP. + MediaPlayPause = Keys.MediaPlayPause, + + /// The MediaPreviousTrack button on a keyboard in Windows 2000/XP. + MediaPreviousTrack = Keys.MediaPreviousTrack, + + /// The MediaStop button on a keyboard in Windows 2000/XP. + MediaStop = Keys.MediaStop, + + /// The Multiply button on a keyboard. + Multiply = Keys.Multiply, + + /// The N button on a keyboard. + N = Keys.N, + + /// The Num Lock button on a keyboard. + NumLock = Keys.NumLock, + + /// The Numeric keypad 0 button on a keyboard. + NumPad0 = Keys.NumPad0, + + /// The Numeric keypad 1 button on a keyboard. + NumPad1 = Keys.NumPad1, + + /// The Numeric keypad 2 button on a keyboard. + NumPad2 = Keys.NumPad2, + + /// The Numeric keypad 3 button on a keyboard. + NumPad3 = Keys.NumPad3, + + /// The Numeric keypad 4 button on a keyboard. + NumPad4 = Keys.NumPad4, + + /// The Numeric keypad 5 button on a keyboard. + NumPad5 = Keys.NumPad5, + + /// The Numeric keypad 6 button on a keyboard. + NumPad6 = Keys.NumPad6, + + /// The Numeric keypad 7 button on a keyboard. + NumPad7 = Keys.NumPad7, + + /// The Numeric keypad 8 button on a keyboard. + NumPad8 = Keys.NumPad8, + + /// The Numeric keypad 9 button on a keyboard. + NumPad9 = Keys.NumPad9, + + /// The O button on a keyboard. + O = Keys.O, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + Oem8 = Keys.Oem8, + + /// The OEM Auto button on a keyboard. + OemAuto = Keys.OemAuto, + + /// The OEM Angle Bracket or Backslash button on the RT 102 keyboard in Windows 2000/XP. + OemBackslash = Keys.OemBackslash, + + /// The Clear button on a keyboard. + OemClear = Keys.OemClear, + + /// The OEM Close Bracket button on a US standard keyboard in Windows 2000/XP. + OemCloseBrackets = Keys.OemCloseBrackets, + + /// The ',' button on a keyboard in any country/region in Windows 2000/XP. + OemComma = Keys.OemComma, + + /// The OEM Copy button on a keyboard. + OemCopy = Keys.OemCopy, + + /// The OEM Enlarge Window button on a keyboard. + OemEnlW = Keys.OemEnlW, + + /// The '-' button on a keyboard in any country/region in Windows 2000/XP. + OemMinus = Keys.OemMinus, + + /// The OEM Open Bracket button on a US standard keyboard in Windows 2000/XP. + OemOpenBrackets = Keys.OemOpenBrackets, + + /// The '.' button on a keyboard in any country/region. + OemPeriod = Keys.OemPeriod, + + /// The OEM Pipe button on a US standard keyboard. + OemPipe = Keys.OemPipe, + + /// The '+' button on a keyboard in Windows 2000/XP. + OemPlus = Keys.OemPlus, + + /// The OEM Question Mark button on a US standard keyboard. + OemQuestion = Keys.OemQuestion, + + /// The OEM Single/Double Quote button on a US standard keyboard. + OemQuotes = Keys.OemQuotes, + + /// The OEM Semicolon button on a US standard keyboard. + OemSemicolon = Keys.OemSemicolon, + + /// The OEM Tilde button on a US standard keyboard. + OemTilde = Keys.OemTilde, + + /// The P button on a keyboard. + P = Keys.P, + + /// The PA1 button on a keyboard. + Pa1 = Keys.Pa1, + + /// The Page Down button on a keyboard. + PageDown = Keys.PageDown, + + /// The Page Up button on a keyboard. + PageUp = Keys.PageUp, + + /// The Pause button on a keyboard. + Pause = Keys.Pause, + + /// The Play button on a keyboard. + Play = Keys.Play, + + /// The Print button on a keyboard. + Print = Keys.Print, + + /// The Print Screen button on a keyboard. + PrintScreen = Keys.PrintScreen, + + /// The IME Process button on a keyboard in Windows 95/98/ME/NT 4.0/2000/XP. + ProcessKey = Keys.ProcessKey, + + /// The Q button on a keyboard. + Q = Keys.Q, + + /// The R button on a keyboard. + R = Keys.R, + + /// The Right Arrow button on a keyboard. + Right = Keys.Right, + + /// The Right Alt button on a keyboard. + RightAlt = Keys.RightAlt, + + /// The Right Control button on a keyboard. + RightControl = Keys.RightControl, + + /// The Right Shift button on a keyboard. + RightShift = Keys.RightShift, + + /// The Right Windows button on a keyboard. + RightWindows = Keys.RightWindows, + + /// The S button on a keyboard. + S = Keys.S, + + /// The Scroll Lock button on a keyboard. + Scroll = Keys.Scroll, + + /// The Select button on a keyboard. + Select = Keys.Select, + + /// The Select Media button on a keyboard in Windows 2000/XP. + SelectMedia = Keys.SelectMedia, + + /// The Separator button on a keyboard. + Separator = Keys.Separator, + + /// The Computer Sleep button on a keyboard. + Sleep = Keys.Sleep, + + /// The Space bar on a keyboard. + Space = Keys.Space, + + /// The Subtract button on a keyboard. + Subtract = Keys.Subtract, + + /// The T button on a keyboard. + T = Keys.T, + + /// The Tab button on a keyboard. + Tab = Keys.Tab, + + /// The U button on a keyboard. + U = Keys.U, + + /// The Up Arrow button on a keyboard. + Up = Keys.Up, + + /// The V button on a keyboard. + V = Keys.V, + + /// The Volume Down button on a keyboard in Windows 2000/XP. + VolumeDown = Keys.VolumeDown, + + /// The Volume Mute button on a keyboard in Windows 2000/XP. + VolumeMute = Keys.VolumeMute, + + /// The Volume Up button on a keyboard in Windows 2000/XP. + VolumeUp = Keys.VolumeUp, + + /// The W button on a keyboard. + W = Keys.W, + + /// The X button on a keyboard. + X = Keys.X, + + /// The Y button on a keyboard. + Y = Keys.Y, + + /// The Z button on a keyboard. + Z = Keys.Z, + + /// The Zoom button on a keyboard. + Zoom = Keys.Zoom + } + + /// Provides extension methods for . +#if SMAPI_2_0 + public +#else + internal +#endif + static class SButtonExtensions + { + /********* + ** Accessors + *********/ + /// The offset added to values when converting them to to avoid collisions with values. + internal const int ControllerOffset = 2000; + + + /********* + ** Public methods + *********/ + /// Get the equivalent for the given button. + /// The keyboard button to convert. + internal static SButton ToSButton(this Keys key) + { + return (SButton)key; + } + + /// Get the equivalent for the given button. + /// The controller button to convert. + internal static SButton ToSButton(this Buttons key) + { + return (SButton)(SButtonExtensions.ControllerOffset + key); + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The keyboard equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetKeyboard(this SButton input, out Keys key) + { + if (Enum.IsDefined(typeof(Keys), (int)input)) + { + key = (Keys)input; + return true; + } + + key = Keys.None; + return false; + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The controller equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetController(this SButton input, out Buttons button) + { + if (Enum.IsDefined(typeof(Keys), (int)input - SButtonExtensions.ControllerOffset)) + { + button = (Buttons)(input - SButtonExtensions.ControllerOffset); + return true; + } + + button = 0; + return false; + } + } +} -- cgit From c5e106801e9137078decfd6b6e3761240b47f94e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 11:29:17 -0400 Subject: split reflection logic out of mod helper (#318) --- .../Framework/InternalExtensions.cs | 5 +- .../Framework/Reflection/ReflectionHelper.cs | 316 --------------------- .../Framework/Reflection/Reflector.cs | 276 ++++++++++++++++++ src/StardewModdingAPI/Framework/SContentManager.cs | 2 +- src/StardewModdingAPI/Framework/SGame.cs | 5 +- src/StardewModdingAPI/Program.cs | 5 +- src/StardewModdingAPI/ReflectionHelper.cs | 158 +++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 3 +- 8 files changed, 446 insertions(+), 324 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/Reflector.cs create mode 100644 src/StardewModdingAPI/ReflectionHelper.cs (limited to 'src/StardewModdingAPI/Framework/SGame.cs') diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index b99d3798..2842bc83 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; using StardewValley; namespace StardewModdingAPI.Framework @@ -99,7 +100,7 @@ namespace StardewModdingAPI.Framework /// Get whether the sprite batch is between a begin and end pair. /// The sprite batch to check. /// The reflection helper with which to access private fields. - public static bool IsOpen(this SpriteBatch spriteBatch, IReflectionHelper reflection) + public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection) { // get field name const string fieldName = @@ -110,7 +111,7 @@ namespace StardewModdingAPI.Framework #endif // get result - return reflection.GetPrivateValue(Game1.spriteBatch, fieldName); + return reflection.GetPrivateField(Game1.spriteBatch, fieldName).GetValue(); } } } diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs deleted file mode 100644 index 7a5789dc..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs +++ /dev/null @@ -1,316 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.Caching; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// Provides helper methods for accessing private game code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). - internal class ReflectionHelper : IReflectionHelper - { - /********* - ** Properties - *********/ - /// The cached fields and methods found via reflection. - private readonly MemoryCache Cache = new MemoryCache(typeof(ReflectionHelper).FullName); - - /// The sliding cache expiration time. - private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); - - - /********* - ** Public methods - *********/ - /**** - ** Fields - ****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field wrapper, or null if the field doesn't exist and is false. - public IPrivateField GetPrivateField(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); - - // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && field == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); - return field; - } - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) - { - // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && field == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); - return field; - } - - /**** - ** Properties - ****/ - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); - - // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && property == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); - return property; - } - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) - { - // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && property == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); - return property; - } - - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(object obj, string name, bool required = true) - { - IPrivateField field = this.GetPrivateField(obj, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(Type type, string name, bool required = true) - { - IPrivateField field = this.GetPrivateField(type, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /**** - ** Methods - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); - - // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); - return method; - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) - { - // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); - return method; - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - // validate parent - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); - - // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); - if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); - return method; - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); - if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); - return method; - } - - - /********* - ** Private methods - *********/ - /// Get a field from the type hierarchy. - /// The expected field type. - /// The type which has the field. - /// The object which has the field. - /// The field name. - /// The reflection binding which flags which indicates what type of field to find. - private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => - { - FieldInfo fieldInfo = null; - for (; type != null && fieldInfo == null; type = type.BaseType) - fieldInfo = type.GetField(name, bindingFlags); - return fieldInfo; - }); - - return field != null - ? new PrivateField(type, obj, field, isStatic) - : null; - } - - /// Get a property from the type hierarchy. - /// The expected property type. - /// The type which has the property. - /// The object which has the property. - /// The property name. - /// The reflection binding which flags which indicates what type of property to find. - private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => - { - PropertyInfo propertyInfo = null; - for (; type != null && propertyInfo == null; type = type.BaseType) - propertyInfo = type.GetProperty(name, bindingFlags); - return propertyInfo; - }); - - return property != null - ? new PrivateProperty(type, obj, property, isStatic) - : null; - } - - /// Get a method from the type hierarchy. - /// The type which has the method. - /// The object which has the method. - /// The method name. - /// The reflection binding which flags which indicates what type of method to find. - private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => - { - MethodInfo methodInfo = null; - for (; type != null && methodInfo == null; type = type.BaseType) - methodInfo = type.GetMethod(name, bindingFlags); - return methodInfo; - }); - - return method != null - ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) - : null; - } - - /// Get a method from the type hierarchy. - /// The type which has the method. - /// The object which has the method. - /// The method name. - /// The reflection binding which flags which indicates what type of method to find. - /// The argument types of the method signature to find. - private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => - { - MethodInfo methodInfo = null; - for (; type != null && methodInfo == null; type = type.BaseType) - methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); - return methodInfo; - }); - return method != null - ? new PrivateMethod(type, obj, method, isStatic) - : null; - } - - /// Get a method or field through the cache. - /// The expected type. - /// The cache key. - /// Fetches a new value to cache. - private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo - { - // get from cache - if (this.Cache.Contains(key)) - { - CacheEntry entry = (CacheEntry)this.Cache[key]; - return entry.IsValid - ? (TMemberInfo)entry.MemberInfo - : default(TMemberInfo); - } - - // fetch & cache new value - TMemberInfo result = fetch(); - CacheEntry cacheEntry = new CacheEntry(result != null, result); - this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); - return result; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs new file mode 100644 index 00000000..5c2d90fa --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Caching; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class Reflector + { + /********* + ** Properties + *********/ + /// The cached fields and methods found via reflection. + private readonly MemoryCache Cache = new MemoryCache(typeof(Reflector).FullName); + + /// The sliding cache expiration time. + private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + + + /********* + ** Public methods + *********/ + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); + + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && field == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); + return field; + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && field == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); + return field; + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); + + // get property from hierarchy + IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && property == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); + return property; + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && property == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); + return property; + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); + return method; + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + // validate parent + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + // get field from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); + return method; + } + + + /********* + ** Private methods + *********/ + /// Get a field from the type hierarchy. + /// The expected field type. + /// The type which has the field. + /// The object which has the field. + /// The field name. + /// The reflection binding which flags which indicates what type of field to find. + private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => + { + FieldInfo fieldInfo = null; + for (; type != null && fieldInfo == null; type = type.BaseType) + fieldInfo = type.GetField(name, bindingFlags); + return fieldInfo; + }); + + return field != null + ? new PrivateField(type, obj, field, isStatic) + : null; + } + + /// Get a property from the type hierarchy. + /// The expected property type. + /// The type which has the property. + /// The object which has the property. + /// The property name. + /// The reflection binding which flags which indicates what type of property to find. + private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => + { + PropertyInfo propertyInfo = null; + for (; type != null && propertyInfo == null; type = type.BaseType) + propertyInfo = type.GetProperty(name, bindingFlags); + return propertyInfo; + }); + + return property != null + ? new PrivateProperty(type, obj, property, isStatic) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags); + return methodInfo; + }); + + return method != null + ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + /// The argument types of the method signature to find. + private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); + return methodInfo; + }); + return method != null + ? new PrivateMethod(type, obj, method, isStatic) + : null; + } + + /// Get a method or field through the cache. + /// The expected type. + /// The cache key. + /// Fetches a new value to cache. + private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo + { + // get from cache + if (this.Cache.Contains(key)) + { + CacheEntry entry = (CacheEntry)this.Cache[key]; + return entry.IsValid + ? (TMemberInfo)entry.MemberInfo + : default(TMemberInfo); + } + + // fetch & cache new value + TMemberInfo result = fetch(); + CacheEntry cacheEntry = new CacheEntry(result != null, result); + this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); + return result; + } + } +} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 42c3b0e6..669b0e7a 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Framework throw new ArgumentNullException(nameof(monitor)); // initialise - IReflectionHelper reflection = new ReflectionHelper(); + var reflection = new Reflector(); this.Monitor = monitor; // get underlying fields for interception diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f2c5c0c9..c7784c60 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -150,7 +151,7 @@ namespace StardewModdingAPI.Framework ** Private wrappers ****/ /// Simplifies access to private game code. - private static IReflectionHelper Reflection; + private static Reflector Reflection; // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. @@ -184,7 +185,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging. /// Simplifies access to private game code. - internal SGame(IMonitor monitor, IReflectionHelper reflection) + internal SGame(IMonitor monitor, Reflector reflection) { // initialise this.Monitor = monitor; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 4b50e4fb..3b3f99b3 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); /// Simplifies access to private game code. - private readonly IReflectionHelper Reflection = new ReflectionHelper(); + private readonly Reflector Reflection = new Reflector(); /// The underlying game instance. private SGame GameInstance; @@ -702,7 +702,8 @@ namespace StardewModdingAPI // inject data mod.ModManifest = manifest; - mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); + var reflectionHelper = new ReflectionHelper(this.Reflection); + mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, reflectionHelper); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); #if !SMAPI_2_0 mod.PathOnDisk = metadata.DirectoryPath; diff --git a/src/StardewModdingAPI/ReflectionHelper.cs b/src/StardewModdingAPI/ReflectionHelper.cs new file mode 100644 index 00000000..56754cb4 --- /dev/null +++ b/src/StardewModdingAPI/ReflectionHelper.cs @@ -0,0 +1,158 @@ +using System; +using StardewModdingAPI.Framework.Reflection; + +namespace StardewModdingAPI +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class ReflectionHelper : IReflectionHelper + { + /********* + ** Properties + *********/ + /// The underlying reflection helper. + private readonly Reflector Reflector; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying reflection helper. + public ReflectionHelper(Reflector reflector) + { + this.Reflector = reflector; + } + + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateField(obj, name, required); + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateField(type, name, required); + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateProperty(obj, name, required); + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateProperty(type, name, required); + } + + /**** + ** Field values + ** (shorthand since this is the most common case) + ****/ + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(object obj, string name, bool required = true) + { + IPrivateField field = this.GetPrivateField(obj, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(Type type, string name, bool required = true) + { + IPrivateField field = this.GetPrivateField(type, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateMethod(obj, name, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateMethod(type, name, required); + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index c442cc8a..efef87b1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -149,6 +149,7 @@ + @@ -180,7 +181,7 @@ - + -- cgit