diff options
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 81 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 29 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/IContentManager.cs | 5 | ||||
-rw-r--r-- | src/SMAPI/Framework/CursorPosition.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/EventManager.cs | 118 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ManagedEvent.cs | 20 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ManagedEventBase.cs | 9 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ModEvents.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ModEventsBase.cs | 28 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ModInputEvents.cs | 50 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ModWorldEvents.cs | 64 | ||||
-rw-r--r-- | src/SMAPI/Framework/Input/SInputState.cs | 28 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/InputHelper.cs | 54 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModHelper.cs | 14 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 227 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/LocationTracker.cs | 24 |
17 files changed, 638 insertions, 152 deletions
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 1a57dd22..1336f3e9 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; @@ -156,27 +155,7 @@ namespace StardewModdingAPI.Framework // get cloned asset T data = contentManager.Load<T>(internalKey, language); - switch (data as object) - { - case Texture2D source: - { - int[] pixels = new int[source.Width * source.Height]; - source.GetData(pixels); - - Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); - clone.SetData(pixels); - return (T)(object)clone; - } - - case Dictionary<string, string> source: - return (T)(object)new Dictionary<string, string>(source); - - case Dictionary<int, string> source: - return (T)(object)new Dictionary<int, string>(source); - - default: - return data; - } + return contentManager.CloneIfPossible(data); } /// <summary>Purge assets from the cache that match one of the interceptors.</summary> @@ -199,12 +178,36 @@ namespace StardewModdingAPI.Framework { // check loaders MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); - if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) - return true; + foreach (IAssetLoader loader in loaders) + { + try + { + if ((bool)canLoadGeneric.Invoke(loader, new object[] { asset })) + return true; + } + catch (Exception ex) + { + this.GetModFor(loader).LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } // check editors MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); - return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); + foreach (IAssetEditor editor in editors) + { + try + { + if ((bool)canEditGeneric.Invoke(editor, new object[] { asset })) + return true; + } + catch (Exception ex) + { + this.GetModFor(editor).LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + // asset not affected by a loader or editor + return false; }); } @@ -279,5 +282,33 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Remove(contentManager); } + + /// <summary>Get the mod which registered an asset loader.</summary> + /// <param name="loader">The asset loader.</param> + /// <exception cref="KeyNotFoundException">The given loader couldn't be matched to a mod.</exception> + private IModMetadata GetModFor(IAssetLoader loader) + { + foreach (var pair in this.Loaders) + { + if (pair.Value.Contains(loader)) + return pair.Key; + } + + throw new KeyNotFoundException("This loader isn't associated with a known mod."); + } + + /// <summary>Get the mod which registered an asset editor.</summary> + /// <param name="editor">The asset editor.</param> + /// <exception cref="KeyNotFoundException">The given editor couldn't be matched to a mod.</exception> + private IModMetadata GetModFor(IAssetEditor editor) + { + foreach (var pair in this.Editors) + { + if (pair.Value.Contains(editor)) + return pair.Key; + } + + throw new KeyNotFoundException("This editor isn't associated with a known mod."); + } } } diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index ff0e2de4..18aae05b 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; @@ -109,6 +110,34 @@ namespace StardewModdingAPI.Framework.ContentManagers } + /// <summary>Get a copy of the given asset if supported.</summary> + /// <typeparam name="T">The asset type.</typeparam> + /// <param name="asset">The asset to clone.</param> + public T CloneIfPossible<T>(T asset) + { + switch (asset as object) + { + case Texture2D source: + { + int[] pixels = new int[source.Width * source.Height]; + source.GetData(pixels); + + Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); + clone.SetData(pixels); + return (T)(object)clone; + } + + case Dictionary<string, string> source: + return (T)(object)new Dictionary<string, string>(source); + + case Dictionary<int, string> source: + return (T)(object)new Dictionary<int, string>(source); + + default: + return asset; + } + } + /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="AssertAndNormaliseAssetName"/> instead.</summary> /// <param name="path">The file path to normalise.</param> [Pure] diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index cfedb5af..a53840bc 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -161,7 +161,7 @@ namespace StardewModdingAPI.Framework.ContentManagers T data; try { - data = loader.Load<T>(info); + data = this.CloneIfPossible(loader.Load<T>(info)); this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); } catch (Exception ex) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index aa5be9b6..1eb8b0ac 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -47,6 +47,11 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="value">The asset value.</param> void Inject<T>(string assetName, T value); + /// <summary>Get a copy of the given asset if supported.</summary> + /// <typeparam name="T">The asset type.</typeparam> + /// <param name="asset">The asset to clone.</param> + T CloneIfPossible<T>(T asset); + /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="AssertAndNormaliseAssetName"/> instead.</summary> /// <param name="path">The file path to normalise.</param> [Pure] diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index db02b3d1..6f716746 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// <summary>The raw pixel position, not adjusted for the game zoom.</summary> + public Vector2 RawPixels { get; } + /// <summary>The pixel position relative to the top-left corner of the visible screen.</summary> public Vector2 ScreenPixels { get; } @@ -22,11 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// <summary>Construct an instance.</summary> + /// <param name="rawPixels">The raw pixel position, not adjusted for the game zoom.</param> /// <param name="screenPixels">The pixel position relative to the top-left corner of the visible screen.</param> /// <param name="tile">The tile position relative to the top-left corner of the map.</param> /// <param name="grabTile">The tile position that the game considers under the cursor for purposes of clicking actions.</param> - public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + public CursorPosition(Vector2 rawPixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) { + this.RawPixels = rawPixels; this.ScreenPixels = screenPixels; this.Tile = tile; this.GrabTile = grabTile; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 84036127..9f67244a 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; @@ -10,7 +9,47 @@ namespace StardewModdingAPI.Framework.Events internal class EventManager { /********* - ** Properties + ** Events (new) + *********/ + /**** + ** World + ****/ + /// <summary>Raised after a game location is added or removed.</summary> + public readonly ManagedEvent<WorldLocationListChangedEventArgs> World_LocationListChanged; + + /// <summary>Raised after buildings are added or removed in a location.</summary> + public readonly ManagedEvent<WorldBuildingListChangedEventArgs> World_BuildingListChanged; + + /// <summary>Raised after large terrain features (like bushes) are added or removed in a location.</summary> + public readonly ManagedEvent<WorldLargeTerrainFeatureListChangedEventArgs> World_LargeTerrainFeatureListChanged; + + /// <summary>Raised after NPCs are added or removed in a location.</summary> + public readonly ManagedEvent<WorldNpcListChangedEventArgs> World_NpcListChanged; + + /// <summary>Raised after objects are added or removed in a location.</summary> + public readonly ManagedEvent<WorldObjectListChangedEventArgs> World_ObjectListChanged; + + /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> + public readonly ManagedEvent<WorldTerrainFeatureListChangedEventArgs> World_TerrainFeatureListChanged; + + /**** + ** Input + ****/ + /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<InputButtonPressedArgsInput> Input_ButtonPressed; + + /// <summary>Raised after the player released a button on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<InputButtonReleasedArgsInput> Input_ButtonReleased; + + /// <summary>Raised after the player moves the in-game cursor.</summary> + public readonly ManagedEvent<InputCursorMovedArgsInput> Input_CursorMoved; + + /// <summary>Raised after the player scrolls the mouse wheel.</summary> + public readonly ManagedEvent<InputMouseWheelScrolledEventArgs> Input_MouseWheelScrolled; + + + /********* + ** Events (old) *********/ /**** ** ContentEvents @@ -22,28 +61,28 @@ namespace StardewModdingAPI.Framework.Events ** ControlEvents ****/ /// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary> - public readonly ManagedEvent<EventArgsKeyboardStateChanged> Control_KeyboardChanged; + public readonly ManagedEvent<EventArgsKeyboardStateChanged> Legacy_Control_KeyboardChanged; - /// <summary>Raised when the player presses a keyboard key.</summary> - public readonly ManagedEvent<EventArgsKeyPressed> Control_KeyPressed; + /// <summary>Raised after the player presses a keyboard key.</summary> + public readonly ManagedEvent<EventArgsKeyPressed> Legacy_Control_KeyPressed; - /// <summary>Raised when the player releases a keyboard key.</summary> - public readonly ManagedEvent<EventArgsKeyPressed> Control_KeyReleased; + /// <summary>Raised after the player releases a keyboard key.</summary> + public readonly ManagedEvent<EventArgsKeyPressed> Legacy_Control_KeyReleased; /// <summary>Raised when the <see cref="MouseState"/> changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.</summary> - public readonly ManagedEvent<EventArgsMouseStateChanged> Control_MouseChanged; + public readonly ManagedEvent<EventArgsMouseStateChanged> Legacy_Control_MouseChanged; /// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary> - public readonly ManagedEvent<EventArgsControllerButtonPressed> Control_ControllerButtonPressed; + public readonly ManagedEvent<EventArgsControllerButtonPressed> Legacy_Control_ControllerButtonPressed; /// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary> - public readonly ManagedEvent<EventArgsControllerButtonReleased> Control_ControllerButtonReleased; + public readonly ManagedEvent<EventArgsControllerButtonReleased> Legacy_Control_ControllerButtonReleased; /// <summary>The player pressed a controller trigger button.</summary> - public readonly ManagedEvent<EventArgsControllerTriggerPressed> Control_ControllerTriggerPressed; + public readonly ManagedEvent<EventArgsControllerTriggerPressed> Legacy_Control_ControllerTriggerPressed; /// <summary>The player released a controller trigger button.</summary> - public readonly ManagedEvent<EventArgsControllerTriggerReleased> Control_ControllerTriggerReleased; + public readonly ManagedEvent<EventArgsControllerTriggerReleased> Legacy_Control_ControllerTriggerReleased; /**** ** GameEvents @@ -99,23 +138,23 @@ namespace StardewModdingAPI.Framework.Events /**** ** InputEvents ****/ - /// <summary>Raised when the player presses a button on the keyboard, controller, or mouse.</summary> - public readonly ManagedEvent<EventArgsInput> Input_ButtonPressed; + /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<EventArgsInput> Legacy_Input_ButtonPressed; - /// <summary>Raised when the player releases a keyboard key on the keyboard, controller, or mouse.</summary> - public readonly ManagedEvent<EventArgsInput> Input_ButtonReleased; + /// <summary>Raised after the player releases a keyboard key on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<EventArgsInput> Legacy_Input_ButtonReleased; /**** ** LocationEvents ****/ /// <summary>Raised after a game location is added or removed.</summary> - public readonly ManagedEvent<EventArgsLocationsChanged> Location_LocationsChanged; + public readonly ManagedEvent<EventArgsLocationsChanged> Legacy_Location_LocationsChanged; /// <summary>Raised after buildings are added or removed in a location.</summary> - public readonly ManagedEvent<EventArgsLocationBuildingsChanged> Location_BuildingsChanged; + public readonly ManagedEvent<EventArgsLocationBuildingsChanged> Legacy_Location_BuildingsChanged; /// <summary>Raised after objects are added or removed in a location.</summary> - public readonly ManagedEvent<EventArgsLocationObjectsChanged> Location_ObjectsChanged; + public readonly ManagedEvent<EventArgsLocationObjectsChanged> Legacy_Location_ObjectsChanged; /**** ** MenuEvents @@ -209,17 +248,30 @@ namespace StardewModdingAPI.Framework.Events ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry); ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); - // init events + // init events (new) + this.Input_ButtonPressed = ManageEventOf<InputButtonPressedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf<InputButtonReleasedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.Input_CursorMoved = ManageEventOf<InputCursorMovedArgsInput>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.Input_MouseWheelScrolled = ManageEventOf<InputMouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + + this.World_BuildingListChanged = ManageEventOf<WorldBuildingListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.World_LargeTerrainFeatureListChanged = ManageEventOf<WorldLargeTerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); + this.World_LocationListChanged = ManageEventOf<WorldLocationListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.World_NpcListChanged = ManageEventOf<WorldNpcListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); + this.World_ObjectListChanged = ManageEventOf<WorldObjectListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.World_TerrainFeatureListChanged = ManageEventOf<WorldTerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + + // init events (old) this.Content_LocaleChanged = ManageEventOf<EventArgsValueChanged<string>>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); - this.Control_ControllerButtonPressed = ManageEventOf<EventArgsControllerButtonPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); - this.Control_ControllerButtonReleased = ManageEventOf<EventArgsControllerButtonReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); - this.Control_ControllerTriggerPressed = ManageEventOf<EventArgsControllerTriggerPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); - this.Control_ControllerTriggerReleased = ManageEventOf<EventArgsControllerTriggerReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); - this.Control_KeyboardChanged = ManageEventOf<EventArgsKeyboardStateChanged>(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); - this.Control_KeyPressed = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); - this.Control_KeyReleased = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); - this.Control_MouseChanged = ManageEventOf<EventArgsMouseStateChanged>(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + this.Legacy_Control_ControllerButtonPressed = ManageEventOf<EventArgsControllerButtonPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Legacy_Control_ControllerButtonReleased = ManageEventOf<EventArgsControllerButtonReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Legacy_Control_ControllerTriggerPressed = ManageEventOf<EventArgsControllerTriggerPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Legacy_Control_ControllerTriggerReleased = ManageEventOf<EventArgsControllerTriggerReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Legacy_Control_KeyboardChanged = ManageEventOf<EventArgsKeyboardStateChanged>(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Legacy_Control_KeyPressed = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Legacy_Control_KeyReleased = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Legacy_Control_MouseChanged = ManageEventOf<EventArgsMouseStateChanged>(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); @@ -238,12 +290,12 @@ namespace StardewModdingAPI.Framework.Events this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); - this.Input_ButtonPressed = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); - this.Input_ButtonReleased = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + this.Legacy_Input_ButtonPressed = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Legacy_Input_ButtonReleased = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - this.Location_LocationsChanged = ManageEventOf<EventArgsLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Location_BuildingsChanged = ManageEventOf<EventArgsLocationBuildingsChanged>(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); - this.Location_ObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); + this.Legacy_Location_LocationsChanged = ManageEventOf<EventArgsLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Legacy_Location_BuildingsChanged = ManageEventOf<EventArgsLocationBuildingsChanged>(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); + this.Legacy_Location_ObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf<EventArgsClickableMenuChanged>(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); this.Menu_Closed = ManageEventOf<EventArgsClickableMenuClosed>(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index e54a4fd3..c1ebf6c7 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -28,8 +28,16 @@ namespace StardewModdingAPI.Framework.Events /// <param name="handler">The event handler.</param> public void Add(EventHandler<TEventArgs> handler) { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// <summary>Add an event handler.</summary> + /// <param name="handler">The event handler.</param> + /// <param name="mod">The mod which added the event handler.</param> + public void Add(EventHandler<TEventArgs> handler, IModMetadata mod) + { this.Event += handler; - this.AddTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>()); + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>()); } /// <summary>Remove an event handler.</summary> @@ -85,8 +93,16 @@ namespace StardewModdingAPI.Framework.Events /// <param name="handler">The event handler.</param> public void Add(EventHandler handler) { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// <summary>Add an event handler.</summary> + /// <param name="handler">The event handler.</param> + /// <param name="mod">The mod which added the event handler.</param> + public void Add(EventHandler handler, IModMetadata mod) + { this.Event += handler; - this.AddTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler>()); + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler>()); } /// <summary>Remove an event handler.</summary> diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs index 7e42d613..f3a278dc 100644 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI.Framework.Events private readonly IMonitor Monitor; /// <summary>The mod registry with which to identify mods.</summary> - private readonly ModRegistry ModRegistry; + protected readonly ModRegistry ModRegistry; /// <summary>The display names for the mods which added each delegate.</summary> private readonly IDictionary<TEventHandler, IModMetadata> SourceMods = new Dictionary<TEventHandler, IModMetadata>(); @@ -50,11 +50,12 @@ namespace StardewModdingAPI.Framework.Events } /// <summary>Track an event handler.</summary> + /// <param name="mod">The mod which added the handler.</param> /// <param name="handler">The event handler.</param> /// <param name="invocationList">The updated event invocation list.</param> - protected void AddTracking(TEventHandler handler, IEnumerable<TEventHandler> invocationList) + protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable<TEventHandler> invocationList) { - this.SourceMods[handler] = this.ModRegistry.GetFromStack(); + this.SourceMods[handler] = mod; this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; } @@ -64,7 +65,7 @@ namespace StardewModdingAPI.Framework.Events protected void RemoveTracking(TEventHandler handler, IEnumerable<TEventHandler> invocationList) { this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; - if(!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) + if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs new file mode 100644 index 00000000..90853141 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -0,0 +1,30 @@ +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>Manages access to events raised by SMAPI.</summary> + internal class ModEvents : IModEvents + { + /********* + ** Accessors + *********/ + /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> + public IInputEvents Input { get; } + + /// <summary>Events raised when something changes in the world.</summary> + public IWorldEvents World { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The mod which uses this instance.</param> + /// <param name="eventManager">The underlying event manager.</param> + public ModEvents(IModMetadata mod, EventManager eventManager) + { + this.Input = new ModInputEvents(mod, eventManager); + this.World = new ModWorldEvents(mod, eventManager); + } + } +} diff --git a/src/SMAPI/Framework/Events/ModEventsBase.cs b/src/SMAPI/Framework/Events/ModEventsBase.cs new file mode 100644 index 00000000..545c58a8 --- /dev/null +++ b/src/SMAPI/Framework/Events/ModEventsBase.cs @@ -0,0 +1,28 @@ +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>An internal base class for event API classes.</summary> + internal abstract class ModEventsBase + { + /********* + ** Properties + *********/ + /// <summary>The underlying event manager.</summary> + protected readonly EventManager EventManager; + + /// <summary>The mod which uses this instance.</summary> + protected readonly IModMetadata Mod; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The mod which uses this instance.</param> + /// <param name="eventManager">The underlying event manager.</param> + internal ModEventsBase(IModMetadata mod, EventManager eventManager) + { + this.Mod = mod; + this.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs new file mode 100644 index 00000000..387ea87a --- /dev/null +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -0,0 +1,50 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> + internal class ModInputEvents : ModEventsBase, IInputEvents + { + /********* + ** Accessors + *********/ + /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary> + public event EventHandler<InputButtonPressedArgsInput> ButtonPressed + { + add => this.EventManager.Input_ButtonPressed.Add(value); + remove => this.EventManager.Input_ButtonPressed.Remove(value); + } + + /// <summary>Raised after the player releases a button on the keyboard, controller, or mouse.</summary> + public event EventHandler<InputButtonReleasedArgsInput> ButtonReleased + { + add => this.EventManager.Input_ButtonReleased.Add(value); + remove => this.EventManager.Input_ButtonReleased.Remove(value); + } + + /// <summary>Raised after the player moves the in-game cursor.</summary> + public event EventHandler<InputCursorMovedArgsInput> CursorMoved + { + add => this.EventManager.Input_CursorMoved.Add(value); + remove => this.EventManager.Input_CursorMoved.Remove(value); + } + + /// <summary>Raised after the player scrolls the mouse wheel.</summary> + public event EventHandler<InputMouseWheelScrolledEventArgs> MouseWheelScrolled + { + add => this.EventManager.Input_MouseWheelScrolled.Add(value); + remove => this.EventManager.Input_MouseWheelScrolled.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The mod which uses this instance.</param> + /// <param name="eventManager">The underlying event manager.</param> + internal ModInputEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs new file mode 100644 index 00000000..e1a53e0c --- /dev/null +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -0,0 +1,64 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>Events raised when something changes in the world.</summary> + internal class ModWorldEvents : ModEventsBase, IWorldEvents + { + /********* + ** Accessors + *********/ + /// <summary>Raised after a game location is added or removed.</summary> + public event EventHandler<WorldLocationListChangedEventArgs> LocationListChanged + { + add => this.EventManager.World_LocationListChanged.Add(value, this.Mod); + remove => this.EventManager.World_LocationListChanged.Remove(value); + } + + /// <summary>Raised after buildings are added or removed in a location.</summary> + public event EventHandler<WorldBuildingListChangedEventArgs> BuildingListChanged + { + add => this.EventManager.World_BuildingListChanged.Add(value, this.Mod); + remove => this.EventManager.World_BuildingListChanged.Remove(value); + } + + /// <summary>Raised after large terrain features (like bushes) are added or removed in a location.</summary> + public event EventHandler<WorldLargeTerrainFeatureListChangedEventArgs> LargeTerrainFeatureListChanged + { + add => this.EventManager.World_LargeTerrainFeatureListChanged.Add(value, this.Mod); + remove => this.EventManager.World_LargeTerrainFeatureListChanged.Remove(value); + } + + /// <summary>Raised after NPCs are added or removed in a location.</summary> + public event EventHandler<WorldNpcListChangedEventArgs> NpcListChanged + { + add => this.EventManager.World_NpcListChanged.Add(value); + remove => this.EventManager.World_NpcListChanged.Remove(value); + } + + /// <summary>Raised after objects are added or removed in a location.</summary> + public event EventHandler<WorldObjectListChangedEventArgs> ObjectListChanged + { + add => this.EventManager.World_ObjectListChanged.Add(value); + remove => this.EventManager.World_ObjectListChanged.Remove(value); + } + + /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> + public event EventHandler<WorldTerrainFeatureListChangedEventArgs> TerrainFeatureListChanged + { + add => this.EventManager.World_TerrainFeatureListChanged.Add(value); + remove => this.EventManager.World_TerrainFeatureListChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The mod which uses this instance.</param> + /// <param name="eventManager">The underlying event manager.</param> + internal ModWorldEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 27e40ab4..44fd0618 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -8,7 +8,7 @@ using StardewValley; #pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) namespace StardewModdingAPI.Framework.Input { - /// <summary>A summary of input changes during an update frame.</summary> + /// <summary>Manages the game's input state.</summary> internal sealed class SInputState : InputState { /********* @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Input /// <summary>The maximum amount of direction to ignore for the left thumbstick.</summary> private const float LeftThumbstickDeadZone = 0.2f; + /// <summary>The cursor position on the screen adjusted for the zoom level.</summary> + private CursorPosition CursorPositionImpl; + /********* ** Accessors @@ -39,8 +42,8 @@ namespace StardewModdingAPI.Framework.Input /// <summary>A derivative of <see cref="RealMouse"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary> public MouseState SuppressedMouse { get; private set; } - /// <summary>The mouse position on the screen adjusted for the zoom level.</summary> - public Point MousePosition { get; private set; } + /// <summary>The cursor position on the screen adjusted for the zoom level.</summary> + public ICursorPosition CursorPosition => this.CursorPositionImpl; /// <summary>The buttons which were pressed, held, or released.</summary> public IDictionary<SButton, InputStatus> ActiveButtons { get; private set; } = new Dictionary<SButton, InputStatus>(); @@ -61,7 +64,7 @@ namespace StardewModdingAPI.Framework.Input RealController = this.RealController, RealKeyboard = this.RealKeyboard, RealMouse = this.RealMouse, - MousePosition = this.MousePosition + CursorPositionImpl = this.CursorPositionImpl }; } @@ -78,15 +81,16 @@ namespace StardewModdingAPI.Framework.Input GamePadState realController = GamePad.GetState(PlayerIndex.One); KeyboardState realKeyboard = Keyboard.GetState(); MouseState realMouse = Mouse.GetState(); - Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); + Vector2 cursorRawPixelPos = new Vector2(this.RealMouse.X, this.RealMouse.Y); // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - this.MousePosition = mousePosition; + if (this.CursorPositionImpl?.RawPixels != cursorRawPixelPos) + this.CursorPositionImpl = this.GetCursorPosition(cursorRawPixelPos); // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); @@ -157,6 +161,18 @@ namespace StardewModdingAPI.Framework.Input /********* ** Private methods *********/ + /// <summary>Get the current cursor position.</summary> + /// <remarks>The raw pixel position from the mouse state.</remarks> + private CursorPosition GetCursorPosition(Vector2 rawPixelPos) + { + Vector2 screenPixels = new Vector2((int)(rawPixelPos.X * (1.0 / Game1.options.zoomLevel)), (int)(rawPixelPos.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((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(); + return new CursorPosition(rawPixelPos, screenPixels, tile, grabTile); + } + /// <summary>Whether input should be suppressed in the current context.</summary> private bool ShouldSuppressNow() { diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs new file mode 100644 index 00000000..f4cd12b6 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -0,0 +1,54 @@ +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// <summary>Provides an API for checking and changing input state.</summary> + internal class InputHelper : BaseHelper, IInputHelper + { + /********* + ** Accessors + *********/ + /// <summary>Manages the game's input state.</summary> + private readonly SInputState InputState; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="inputState">Manages the game's input state.</param> + public InputHelper(string modID, SInputState inputState) + : base(modID) + { + this.InputState = inputState; + } + + /// <summary>Get the current cursor position.</summary> + public ICursorPosition GetCursorPosition() + { + return this.InputState.CursorPosition; + } + + /// <summary>Get whether a button is currently pressed.</summary> + /// <param name="button">The button.</param> + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + + /// <summary>Get whether a button is currently suppressed, so the game won't see it.</summary> + /// <param name="button">The button.</param> + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// <summary>Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event.</summary> + /// <param name="button">The button to suppress.</param> + public void Suppress(SButton button) + { + this.InputState.SuppressButtons.Add(button); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 61d2075c..e8726938 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; using StardewModdingAPI.Toolkit.Utilities; @@ -33,9 +35,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>The full path to the mod's folder.</summary> public string DirectoryPath { get; } + /// <summary>Manages access to events raised by SMAPI, which let your mod react when something happens in the game.</summary> + public IModEvents Events { get; } + /// <summary>An API for loading content assets.</summary> public IContentHelper Content { get; } + /// <summary>An API for checking and changing input state.</summary> + public IInputHelper Input { get; } + /// <summary>An API for accessing private game code.</summary> public IReflectionHelper Reflection { get; } @@ -59,6 +67,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modID">The mod's unique ID.</param> /// <param name="modDirectory">The full path to the mod's folder.</param> /// <param name="jsonHelper">Encapsulate SMAPI's JSON parsing.</param> + /// <param name="inputState">Manages the game's input state.</param> + /// <param name="events">Manages access to events raised by SMAPI.</param> /// <param name="contentHelper">An API for loading content assets.</param> /// <param name="commandHelper">An API for managing console commands.</param> /// <param name="modRegistry">an API for fetching metadata about loaded mods.</param> @@ -70,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="deprecationManager">Manages deprecation warnings.</param> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable<IContentPack> contentPacks, Func<string, IManifest, IContentPack> createContentPack, DeprecationManager deprecationManager) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable<IContentPack> contentPacks, Func<string, IManifest, IContentPack> createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -83,6 +93,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DirectoryPath = modDirectory; this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.Input = new InputHelper(modID, inputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); @@ -91,6 +102,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ContentPacks = contentPacks.ToArray(); this.CreateContentPack = createContentPack; this.DeprecationManager = deprecationManager; + this.Events = events; } /**** diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 369f1f40..a4d149f3 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -18,11 +18,14 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; using StardewValley.Locations; using StardewValley.Menus; +using StardewValley.TerrainFeatures; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; +using Object = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -51,9 +54,6 @@ namespace StardewModdingAPI.Framework /// <summary>Manages SMAPI events for mods.</summary> private readonly EventManager Events; - /// <summary>Manages input visible to the game.</summary> - private SInputState Input => (SInputState)Game1.input; - /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary> private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -73,6 +73,15 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="SaveEvents.BeforeCreate"/>.</summary> private bool IsBetweenCreateEvents; + /// <summary>A callback to invoke after the game finishes initialising.</summary> + private readonly Action OnGameInitialised; + + /// <summary>A callback to invoke when the game exits.</summary> + private readonly Action OnGameExiting; + + /// <summary>Simplifies access to private game code.</summary> + private readonly Reflector Reflection; + /**** ** Game state ****/ @@ -80,41 +89,44 @@ namespace StardewModdingAPI.Framework private readonly List<IWatcher> Watchers = new List<IWatcher>(); /// <summary>Tracks changes to the window size.</summary> - private readonly IValueWatcher<Point> WindowSizeWatcher; + private IValueWatcher<Point> WindowSizeWatcher; /// <summary>Tracks changes to the current player.</summary> private PlayerTracker CurrentPlayerTracker; /// <summary>Tracks changes to the time of day (in 24-hour military format).</summary> - private readonly IValueWatcher<int> TimeWatcher; + private IValueWatcher<int> TimeWatcher; /// <summary>Tracks changes to the save ID.</summary> - private readonly IValueWatcher<ulong> SaveIdWatcher; + private IValueWatcher<ulong> SaveIdWatcher; /// <summary>Tracks changes to the game's locations.</summary> - private readonly WorldLocationsTracker LocationsWatcher; + private WorldLocationsTracker LocationsWatcher; /// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary> - private readonly IValueWatcher<IClickableMenu> ActiveMenuWatcher; + private IValueWatcher<IClickableMenu> ActiveMenuWatcher; + + /// <summary>Tracks changes to the cursor position.</summary> + private IValueWatcher<Vector2> CursorWatcher; + + /// <summary>Tracks changes to the mouse wheel scroll.</summary> + private IValueWatcher<int> MouseWheelScrollWatcher; /// <summary>The previous content locale.</summary> private LocalizedContentManager.LanguageCode? PreviousLocale; + /// <summary>The previous cursor position.</summary> + private ICursorPosition PreviousCursorPosition; + /// <summary>An index incremented on every tick and reset every 60th tick (0–59).</summary> private int CurrentUpdateTick; + /// <summary>Whether post-game-startup initialisation has been performed.</summary> + private bool IsInitialised; + /// <summary>Whether this is the very first update tick since the game started.</summary> private bool FirstUpdate; - /// <summary>A callback to invoke after the game finishes initialising.</summary> - private readonly Action OnGameInitialised; - - /// <summary>A callback to invoke when the game exits.</summary> - private readonly Action OnGameExiting; - - /// <summary>Simplifies access to private game code.</summary> - private readonly Reflector Reflection; - /// <summary>Whether the next content manager requested by the game will be for <see cref="Game1.content"/>.</summary> private bool NextContentManagerIsMain; @@ -125,6 +137,9 @@ namespace StardewModdingAPI.Framework /// <summary>SMAPI's content manager.</summary> public ContentCoordinator ContentCore { get; private set; } + /// <summary>Manages input visible to the game.</summary> + public SInputState Input => (SInputState)Game1.input; + /// <summary>The game's core multiplayer utility.</summary> public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; @@ -160,8 +175,19 @@ namespace StardewModdingAPI.Framework Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager); - // init watchers + // init observables Game1.locations = new ObservableCollection<GameLocation>(); + } + + /// <summary>Initialise just before the game's first update tick.</summary> + private void InitialiseAfterGameStarted() + { + // set initial state + this.Input.TrueUpdate(); + + // init watchers + this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); @@ -169,12 +195,17 @@ namespace StardewModdingAPI.Framework this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations); this.Watchers.AddRange(new IWatcher[] { + this.CursorWatcher, + this.MouseWheelScrollWatcher, this.SaveIdWatcher, this.WindowSizeWatcher, this.TimeWatcher, this.ActiveMenuWatcher, this.LocationsWatcher }); + + // raise callback + this.OnGameInitialised(); } /// <summary>Perform cleanup logic when the game exits.</summary> @@ -223,19 +254,24 @@ namespace StardewModdingAPI.Framework try { /********* - ** Update input + ** Special cases *********/ - // This should *always* run, even when suppressing mod events, since the game uses - // this too. For example, doing this after mod event suppression would prevent the - // user from doing anything on the overnight shipping screen. - SInputState previousInputState = this.Input.Clone(); - SInputState inputState = this.Input; - if (this.IsActive) - inputState.TrueUpdate(); + // Perform first-tick initialisation. + if (!this.IsInitialised) + { + this.IsInitialised = true; + this.InitialiseAfterGameStarted(); + } - /********* - ** Load game synchronously - *********/ + // Abort if SMAPI is exiting. + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); + return; + } + + // Load saves synchronously to avoid issues due to mod events triggering + // concurrently with game code. if (Game1.gameMode == Game1.loadingMode) { this.Monitor.Log("Running game loader...", LogLevel.Trace); @@ -247,16 +283,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Game loader OK.", LogLevel.Trace); } - /********* - ** Skip conditions - *********/ - // SMAPI exiting, stop processing game updates - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); - return; - } - // While a background task is in progress, the game may make changes to the game // state while mods are running their code. This is risky, because data changes can // conflict (e.g. collection changed during enumeration errors) and data may change @@ -274,6 +300,17 @@ namespace StardewModdingAPI.Framework } /********* + ** Update input + *********/ + // This should *always* run, even when suppressing mod events, since the game uses + // this too. For example, doing this after mod event suppression would prevent the + // user from doing anything on the overnight shipping screen. + SInputState previousInputState = this.Input.Clone(); + SInputState inputState = this.Input; + if (this.IsActive) + inputState.TrueUpdate(); + + /********* ** Save events + suppress events during save *********/ // While the game is writing to the save file in the background, mods can unexpectedly @@ -321,12 +358,6 @@ namespace StardewModdingAPI.Framework } /********* - ** Notify SMAPI that game is initialised - *********/ - if (this.FirstUpdate) - this.OnGameInitialised(); - - /********* ** Update context *********/ if (Context.IsWorldReady && !Context.IsSaveLoaded) @@ -436,19 +467,27 @@ namespace StardewModdingAPI.Framework bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); if (!isChatInput) { - // get cursor position - ICursorPosition cursor; + ICursorPosition cursor = this.Input.CursorPosition; + + // raise cursor moved event + if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) { - // cursor position - Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((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); + this.CursorWatcher.Reset(); + this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor)); } + this.PreviousCursorPosition = cursor; - // raise input events + // raise mouse wheel scrolled + if (this.MouseWheelScrollWatcher.IsChanged) + { + int oldValue = this.MouseWheelScrollWatcher.PreviousValue; + int newValue = this.MouseWheelScrollWatcher.CurrentValue; + this.MouseWheelScrollWatcher.Reset(); + + this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, oldValue, newValue)); + } + + // raise input button events foreach (var pair in inputState.ActiveButtons) { SButton button = pair.Key; @@ -456,47 +495,49 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new InputButtonPressedArgsInput(button, cursor, inputState)); + this.Events.Legacy_Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); + this.Events.Legacy_Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new InputButtonReleasedArgsInput(button, cursor, inputState)); + this.Events.Legacy_Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); + this.Events.Legacy_Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + this.Events.Legacy_Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); else - this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + this.Events.Legacy_Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.RealKeyboard != previousInputState.RealKeyboard) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, previousInputState.MousePosition, inputState.MousePosition)); + this.Events.Legacy_Control_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); } } @@ -543,7 +584,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } - this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); + this.Events.World_LocationListChanged.Raise(new WorldLocationListChangedEventArgs(added, removed)); + this.Events.Legacy_Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); } // raise location contents changed @@ -551,26 +593,61 @@ namespace StardewModdingAPI.Framework { foreach (LocationTracker watcher in this.LocationsWatcher.Locations) { + // buildings changed + if (watcher.BuildingsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + Building[] added = watcher.BuildingsWatcher.Added.ToArray(); + Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); + watcher.BuildingsWatcher.Reset(); + + this.Events.World_BuildingListChanged.Raise(new WorldBuildingListChangedEventArgs(location, added, removed)); + this.Events.Legacy_Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); + } + + // large terrain features changed + if (watcher.LargeTerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); + LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); + watcher.LargeTerrainFeaturesWatcher.Reset(); + + this.Events.World_LargeTerrainFeatureListChanged.Raise(new WorldLargeTerrainFeatureListChangedEventArgs(location, added, removed)); + } + + // NPCs changed + if (watcher.NpcsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + NPC[] added = watcher.NpcsWatcher.Added.ToArray(); + NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); + watcher.NpcsWatcher.Reset(); + + this.Events.World_NpcListChanged.Raise(new WorldNpcListChangedEventArgs(location, added, removed)); + } + // objects changed if (watcher.ObjectsWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.ObjectsWatcher.Added.ToArray(); - var removed = watcher.ObjectsWatcher.Removed.ToArray(); + KeyValuePair<Vector2, Object>[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair<Vector2, Object>[] removed = watcher.ObjectsWatcher.Removed.ToArray(); watcher.ObjectsWatcher.Reset(); - this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); + this.Events.World_ObjectListChanged.Raise(new WorldObjectListChangedEventArgs(location, added, removed)); + this.Events.Legacy_Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); } - // buildings changed - if (watcher.BuildingsWatcher.IsChanged) + // terrain features changed + if (watcher.TerrainFeaturesWatcher.IsChanged) { GameLocation location = watcher.Location; - var added = watcher.BuildingsWatcher.Added.ToArray(); - var removed = watcher.BuildingsWatcher.Removed.ToArray(); - watcher.BuildingsWatcher.Reset(); + KeyValuePair<Vector2, TerrainFeature>[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); + KeyValuePair<Vector2, TerrainFeature>[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); + watcher.TerrainFeaturesWatcher.Reset(); - this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); + this.Events.World_TerrainFeatureListChanged.Raise(new WorldTerrainFeatureListChangedEventArgs(location, added, removed)); } } } diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 07570401..1b4c0b19 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -6,6 +6,7 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; +using StardewValley.TerrainFeatures; using Object = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -29,12 +30,21 @@ namespace StardewModdingAPI.Framework.StateTracking /// <summary>The tracked location.</summary> public GameLocation Location { get; } - /// <summary>Tracks changes to the location's buildings.</summary> + /// <summary>Tracks added or removed buildings.</summary> public ICollectionWatcher<Building> BuildingsWatcher { get; } - /// <summary>Tracks changes to the location's objects.</summary> + /// <summary>Tracks added or removed large terrain features.</summary> + public ICollectionWatcher<LargeTerrainFeature> LargeTerrainFeaturesWatcher { get; } + + /// <summary>Tracks added or removed NPCs.</summary> + public ICollectionWatcher<NPC> NpcsWatcher { get; } + + /// <summary>Tracks added or removed objects.</summary> public IDictionaryWatcher<Vector2, Object> ObjectsWatcher { get; } + /// <summary>Tracks added or removed terrain features.</summary> + public IDictionaryWatcher<Vector2, TerrainFeature> TerrainFeaturesWatcher { get; } + /********* ** Public methods @@ -46,15 +56,21 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : (ICollectionWatcher<Building>)WatcherFactory.ForObservableCollection(new ObservableCollection<Building>()); + this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures); + this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); this.Watchers.AddRange(new IWatcher[] { this.BuildingsWatcher, - this.ObjectsWatcher + this.LargeTerrainFeaturesWatcher, + this.NpcsWatcher, + this.ObjectsWatcher, + this.TerrainFeaturesWatcher }); } |