summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md13
-rw-r--r--src/SMAPI.Web/Startup.cs9
-rw-r--r--src/SMAPI.Web/appsettings.json2
-rw-r--r--src/SMAPI/Events/ControlEvents.cs36
-rw-r--r--src/SMAPI/Events/EventArgsInput.cs10
-rw-r--r--src/SMAPI/Events/EventArgsLocationObjectsChanged.cs1
-rw-r--r--src/SMAPI/Events/IInputEvents.cs20
-rw-r--r--src/SMAPI/Events/IModEvents.cs12
-rw-r--r--src/SMAPI/Events/IWorldEvents.cs26
-rw-r--r--src/SMAPI/Events/InputButtonPressedEventArgs.cs60
-rw-r--r--src/SMAPI/Events/InputButtonReleasedEventArgs.cs60
-rw-r--r--src/SMAPI/Events/InputCursorMovedEventArgs.cs30
-rw-r--r--src/SMAPI/Events/InputEvents.cs8
-rw-r--r--src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs38
-rw-r--r--src/SMAPI/Events/LocationEvents.cs12
-rw-r--r--src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs39
-rw-r--r--src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs39
-rw-r--r--src/SMAPI/Events/WorldLocationListChangedEventArgs.cs33
-rw-r--r--src/SMAPI/Events/WorldNpcListChangedEventArgs.cs38
-rw-r--r--src/SMAPI/Events/WorldObjectListChangedEventArgs.cs40
-rw-r--r--src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs40
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs81
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs29
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs2
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs5
-rw-r--r--src/SMAPI/Framework/CursorPosition.cs7
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs118
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs20
-rw-r--r--src/SMAPI/Framework/Events/ManagedEventBase.cs9
-rw-r--r--src/SMAPI/Framework/Events/ModEvents.cs30
-rw-r--r--src/SMAPI/Framework/Events/ModEventsBase.cs28
-rw-r--r--src/SMAPI/Framework/Events/ModInputEvents.cs50
-rw-r--r--src/SMAPI/Framework/Events/ModWorldEvents.cs64
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs28
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs54
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs14
-rw-r--r--src/SMAPI/Framework/SGame.cs227
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs24
-rw-r--r--src/SMAPI/IInputHelper.cs21
-rw-r--r--src/SMAPI/IModHelper.cs5
-rw-r--r--src/SMAPI/Program.cs5
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj19
-rw-r--r--src/SMAPI/StardewModdingAPI.metadata.json11
43 files changed, 1211 insertions, 206 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 2a7835c0..8824c0fb 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -15,9 +15,11 @@
* Fixed `world_setseason` command not running season-change logic.
* Fixed mod update checks failing if a mod only has prerelease versions on GitHub.
* Fixed launch issue for Linux players with some terminals. (Thanks to HanFox and kurumushi!)
+ * Fixed Nexus mod update alerts not showing HTTPS links.
* Renamed `install.exe` to `install on Windows.exe` to avoid confusion.
* For modders:
+ * Added [input API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input) for reading and suppressing keyboard, controller, and mouse input.
* Added code analysis to mod build config package to flag common issues as warnings.
* Replaced `LocationEvents` with a more powerful set of events for multiplayer:
* now raised for all locations;
@@ -37,6 +39,7 @@
* Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`.
* Fixed mods able to intercept other mods' assets via the internal asset keys.
* Fixed mods able to indirectly change other mods' data through shared content caches.
+ * Fixed issue where a mod crashing in `CanEdit` or `CanLoad` could cause an abort-retry loop.
* **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)):
* Dropped some deprecated APIs.
* `LocationEvents` have been rewritten (see above).
@@ -259,7 +262,7 @@
* **New features for modders**
SMAPI 2.0 adds several features to enable new kinds of mods (see
- [API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)).
+ [API documentation](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs)).
The **content API** lets you edit, inject, and reload XNB data loaded by the game at any time. This lets SMAPI mods do
anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g.
@@ -382,8 +385,8 @@ For players:
* Updated mod compatibility list.
For modders:
-* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)).
-* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)).
+* Added `SDate` utility for in-game date calculations (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Utilities#Dates)).
+* Added support for minimum dependency versions in `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)).
* Added more useful logging when loading mods.
* Added a `ModID` property to all mod helpers for extension methods.
* Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code.
@@ -415,8 +418,8 @@ For players:
* Updated mod compatibility list.
For modders:
-* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)).
-* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:SMAPI_APIs#Translation)).
+* You can now add dependencies to `manifest.json` (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest)).
+* You can now translate your mod (see [API reference](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation)).
* You can now load unpacked `.tbin` files from your mod folder through the content API.
* SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder.
<small>_When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._</small>
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 7f5b8e2b..82a0f4f6 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -161,16 +161,15 @@ namespace StardewModdingAPI.Web
// redirect legacy canimod.com URLs
var wikiRedirects = new Dictionary<string, string[]>
{
- ["Modding:Creating_a_SMAPI_mod"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod" },
+ ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
+ ["Modding:Modder_Guide"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod", "^/for-devs/creating-a-smapi-mod-advanced-config" },
+ ["Modding:Player_Guide"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods", "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },
+
["Modding:Editing_XNB_files"] = new[] { "^/for-devs/creating-an-xnb-mod", "^/guides/creating-an-xnb-mod" },
["Modding:Event_data"] = new[] { "^/for-devs/events", "^/guides/events" },
["Modding:Gift_taste_data"] = new[] { "^/for-devs/npc-gift-tastes", "^/guides/npc-gift-tastes" },
["Modding:IDE_reference"] = new[] { "^/for-devs/creating-a-smapi-mod-ide-primer" },
- ["Modding:Installing_SMAPI"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods" },
["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
- ["Modding:Player_FAQs"] = new[] { "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },
- ["Modding:SMAPI_APIs"] = new[] { "^/for-devs/creating-a-smapi-mod-advanced-config" },
- ["Modding:Updating_deprecated_SMAPI_code"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
};
foreach (KeyValuePair<string, string[]> pair in wikiRedirects)
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 095707a8..53da6307 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -31,7 +31,7 @@
"GitHubPassword": null, // see top note
"NexusUserAgent": "Nexus Client v0.63.15",
- "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
+ "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley",
"NexusModUrlFormat": "mods/{0}",
"PastebinBaseUrl": "https://pastebin.com/",
diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs
index 973bb245..a3994d1d 100644
--- a/src/SMAPI/Events/ControlEvents.cs
+++ b/src/SMAPI/Events/ControlEvents.cs
@@ -20,57 +20,57 @@ namespace StardewModdingAPI.Events
/// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary>
public static event EventHandler<EventArgsKeyboardStateChanged> KeyboardChanged
{
- add => ControlEvents.EventManager.Control_KeyboardChanged.Add(value);
- remove => ControlEvents.EventManager.Control_KeyboardChanged.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_KeyboardChanged.Remove(value);
}
- /// <summary>Raised when the player presses a keyboard key.</summary>
+ /// <summary>Raised after the player presses a keyboard key.</summary>
public static event EventHandler<EventArgsKeyPressed> KeyPressed
{
- add => ControlEvents.EventManager.Control_KeyPressed.Add(value);
- remove => ControlEvents.EventManager.Control_KeyPressed.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_KeyPressed.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_KeyPressed.Remove(value);
}
- /// <summary>Raised when the player releases a keyboard key.</summary>
+ /// <summary>Raised after the player releases a keyboard key.</summary>
public static event EventHandler<EventArgsKeyPressed> KeyReleased
{
- add => ControlEvents.EventManager.Control_KeyReleased.Add(value);
- remove => ControlEvents.EventManager.Control_KeyReleased.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_KeyReleased.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_KeyReleased.Remove(value);
}
/// <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 static event EventHandler<EventArgsMouseStateChanged> MouseChanged
{
- add => ControlEvents.EventManager.Control_MouseChanged.Add(value);
- remove => ControlEvents.EventManager.Control_MouseChanged.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_MouseChanged.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_MouseChanged.Remove(value);
}
/// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary>
public static event EventHandler<EventArgsControllerButtonPressed> ControllerButtonPressed
{
- add => ControlEvents.EventManager.Control_ControllerButtonPressed.Add(value);
- remove => ControlEvents.EventManager.Control_ControllerButtonPressed.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonPressed.Remove(value);
}
/// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary>
public static event EventHandler<EventArgsControllerButtonReleased> ControllerButtonReleased
{
- add => ControlEvents.EventManager.Control_ControllerButtonReleased.Add(value);
- remove => ControlEvents.EventManager.Control_ControllerButtonReleased.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_ControllerButtonReleased.Remove(value);
}
/// <summary>The player pressed a controller trigger button.</summary>
public static event EventHandler<EventArgsControllerTriggerPressed> ControllerTriggerPressed
{
- add => ControlEvents.EventManager.Control_ControllerTriggerPressed.Add(value);
- remove => ControlEvents.EventManager.Control_ControllerTriggerPressed.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerPressed.Remove(value);
}
/// <summary>The player released a controller trigger button.</summary>
public static event EventHandler<EventArgsControllerTriggerReleased> ControllerTriggerReleased
{
- add => ControlEvents.EventManager.Control_ControllerTriggerReleased.Add(value);
- remove => ControlEvents.EventManager.Control_ControllerTriggerReleased.Remove(value);
+ add => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Add(value);
+ remove => ControlEvents.EventManager.Legacy_Control_ControllerTriggerReleased.Remove(value);
}
diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs
index d60f4017..0cafdba5 100644
--- a/src/SMAPI/Events/EventArgsInput.cs
+++ b/src/SMAPI/Events/EventArgsInput.cs
@@ -23,10 +23,10 @@ namespace StardewModdingAPI.Events
public ICursorPosition Cursor { get; }
/// <summary>Whether the input should trigger actions on the affected tile.</summary>
- public bool IsActionButton { get; }
+ public bool IsActionButton => this.Button.IsActionButton();
/// <summary>Whether the input should use tools on the affected tile.</summary>
- public bool IsUseToolButton { get; }
+ public bool IsUseToolButton => this.Button.IsUseToolButton();
/// <summary>Whether a mod has indicated the key was already handled.</summary>
public bool IsSuppressed => this.SuppressButtons.Contains(this.Button);
@@ -38,15 +38,11 @@ namespace StardewModdingAPI.Events
/// <summary>Construct an instance.</summary>
/// <param name="button">The button on the controller, keyboard, or mouse.</param>
/// <param name="cursor">The cursor position.</param>
- /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param>
- /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param>
/// <param name="suppressButtons">The buttons to suppress.</param>
- public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton, HashSet<SButton> suppressButtons)
+ public EventArgsInput(SButton button, ICursorPosition cursor, HashSet<SButton> suppressButtons)
{
this.Button = button;
this.Cursor = cursor;
- this.IsActionButton = isActionButton;
- this.IsUseToolButton = isUseToolButton;
this.SuppressButtons = suppressButtons;
}
diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs
index 410ef6e6..3bb387d5 100644
--- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs
+++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
-using Netcode;
using StardewValley;
using SObject = StardewValley.Object;
diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs
new file mode 100644
index 00000000..64d82c57
--- /dev/null
+++ b/src/SMAPI/Events/IInputEvents.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
+ public interface IInputEvents
+ {
+ /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary>
+ event EventHandler<InputButtonPressedArgsInput> ButtonPressed;
+
+ /// <summary>Raised after the player releases a button on the keyboard, controller, or mouse.</summary>
+ event EventHandler<InputButtonReleasedArgsInput> ButtonReleased;
+
+ /// <summary>Raised after the player moves the in-game cursor.</summary>
+ event EventHandler<InputCursorMovedArgsInput> CursorMoved;
+
+ /// <summary>Raised after the player scrolls the mouse wheel.</summary>
+ event EventHandler<InputMouseWheelScrolledEventArgs> MouseWheelScrolled;
+ }
+}
diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs
new file mode 100644
index 00000000..16ec6557
--- /dev/null
+++ b/src/SMAPI/Events/IModEvents.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Manages access to events raised by SMAPI.</summary>
+ public interface IModEvents
+ {
+ /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary>
+ IInputEvents Input { get; }
+
+ /// <summary>Events raised when something changes in the world.</summary>
+ IWorldEvents World { get; }
+ }
+}
diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs
new file mode 100644
index 00000000..067a79bc
--- /dev/null
+++ b/src/SMAPI/Events/IWorldEvents.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Events raised when something changes in the world.</summary>
+ public interface IWorldEvents
+ {
+ /// <summary>Raised after a game location is added or removed.</summary>
+ event EventHandler<WorldLocationListChangedEventArgs> LocationListChanged;
+
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ event EventHandler<WorldBuildingListChangedEventArgs> BuildingListChanged;
+
+ /// <summary>Raised after large terrain features (like bushes) are added or removed in a location.</summary>
+ event EventHandler<WorldLargeTerrainFeatureListChangedEventArgs> LargeTerrainFeatureListChanged;
+
+ /// <summary>Raised after NPCs are added or removed in a location.</summary>
+ event EventHandler<WorldNpcListChangedEventArgs> NpcListChanged;
+
+ /// <summary>Raised after objects are added or removed in a location.</summary>
+ event EventHandler<WorldObjectListChangedEventArgs> ObjectListChanged;
+
+ /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
+ event EventHandler<WorldTerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
+ }
+}
diff --git a/src/SMAPI/Events/InputButtonPressedEventArgs.cs b/src/SMAPI/Events/InputButtonPressedEventArgs.cs
new file mode 100644
index 00000000..002f7cf1
--- /dev/null
+++ b/src/SMAPI/Events/InputButtonPressedEventArgs.cs
@@ -0,0 +1,60 @@
+using System;
+using StardewModdingAPI.Framework.Input;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when a button is pressed.</summary>
+ public class InputButtonPressedArgsInput : EventArgs
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The game's current input state.</summary>
+ private readonly SInputState InputState;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The button on the controller, keyboard, or mouse.</summary>
+ public SButton Button { get; }
+
+ /// <summary>The current cursor position.</summary>
+ public ICursorPosition Cursor { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="button">The button on the controller, keyboard, or mouse.</param>
+ /// <param name="cursor">The cursor position.</param>
+ /// <param name="inputState">The game's current input state.</param>
+ internal InputButtonPressedArgsInput(SButton button, ICursorPosition cursor, SInputState inputState)
+ {
+ this.Button = button;
+ this.Cursor = cursor;
+ this.InputState = inputState;
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ public bool IsSuppressed()
+ {
+ return this.IsSuppressed(this.Button);
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsSuppressed(SButton button)
+ {
+ return this.InputState.SuppressButtons.Contains(button);
+ }
+
+ /// <summary>Get whether a given button was pressed or held.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.InputState.IsDown(button);
+ }
+ }
+}
diff --git a/src/SMAPI/Events/InputButtonReleasedEventArgs.cs b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs
new file mode 100644
index 00000000..bc5e4a89
--- /dev/null
+++ b/src/SMAPI/Events/InputButtonReleasedEventArgs.cs
@@ -0,0 +1,60 @@
+using System;
+using StardewModdingAPI.Framework.Input;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when a button is released.</summary>
+ public class InputButtonReleasedArgsInput : EventArgs
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The game's current input state.</summary>
+ private readonly SInputState InputState;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The button on the controller, keyboard, or mouse.</summary>
+ public SButton Button { get; }
+
+ /// <summary>The current cursor position.</summary>
+ public ICursorPosition Cursor { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="button">The button on the controller, keyboard, or mouse.</param>
+ /// <param name="cursor">The cursor position.</param>
+ /// <param name="inputState">The game's current input state.</param>
+ internal InputButtonReleasedArgsInput(SButton button, ICursorPosition cursor, SInputState inputState)
+ {
+ this.Button = button;
+ this.Cursor = cursor;
+ this.InputState = inputState;
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ public bool IsSuppressed()
+ {
+ return this.IsSuppressed(this.Button);
+ }
+
+ /// <summary>Whether a mod has indicated the key was already handled, so the game should handle it.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsSuppressed(SButton button)
+ {
+ return this.InputState.SuppressButtons.Contains(button);
+ }
+
+ /// <summary>Get whether a given button was pressed or held.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.InputState.IsDown(button);
+ }
+ }
+}
diff --git a/src/SMAPI/Events/InputCursorMovedEventArgs.cs b/src/SMAPI/Events/InputCursorMovedEventArgs.cs
new file mode 100644
index 00000000..02e1ee2c
--- /dev/null
+++ b/src/SMAPI/Events/InputCursorMovedEventArgs.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when the in-game cursor is moved.</summary>
+ public class InputCursorMovedArgsInput : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The previous cursor position.</summary>
+ public ICursorPosition OldPosition { get; }
+
+ /// <summary>The current cursor position.</summary>
+ public ICursorPosition NewPosition { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="oldPosition">The previous cursor position.</param>
+ /// <param name="newPosition">The new cursor position.</param>
+ public InputCursorMovedArgsInput(ICursorPosition oldPosition, ICursorPosition newPosition)
+ {
+ this.OldPosition = oldPosition;
+ this.NewPosition = newPosition;
+ }
+ }
+}
diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs
index 84d7ce5d..e62d6ee6 100644
--- a/src/SMAPI/Events/InputEvents.cs
+++ b/src/SMAPI/Events/InputEvents.cs
@@ -19,15 +19,15 @@ namespace StardewModdingAPI.Events
/// <summary>Raised when the player presses a button on the keyboard, controller, or mouse.</summary>
public static event EventHandler<EventArgsInput> ButtonPressed
{
- add => InputEvents.EventManager.Input_ButtonPressed.Add(value);
- remove => InputEvents.EventManager.Input_ButtonPressed.Remove(value);
+ add => InputEvents.EventManager.Legacy_Input_ButtonPressed.Add(value);
+ remove => InputEvents.EventManager.Legacy_Input_ButtonPressed.Remove(value);
}
/// <summary>Raised when the player releases a keyboard key on the keyboard, controller, or mouse.</summary>
public static event EventHandler<EventArgsInput> ButtonReleased
{
- add => InputEvents.EventManager.Input_ButtonReleased.Add(value);
- remove => InputEvents.EventManager.Input_ButtonReleased.Remove(value);
+ add => InputEvents.EventManager.Legacy_Input_ButtonReleased.Add(value);
+ remove => InputEvents.EventManager.Legacy_Input_ButtonReleased.Remove(value);
}
diff --git a/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs
new file mode 100644
index 00000000..9afab9cc
--- /dev/null
+++ b/src/SMAPI/Events/InputMouseWheelScrolledEventArgs.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments when the player scrolls the mouse wheel.</summary>
+ public class InputMouseWheelScrolledEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The cursor position.</summary>
+ public ICursorPosition Position { get; }
+
+ /// <summary>The old scroll value.</summary>
+ public int OldValue { get; }
+
+ /// <summary>The new scroll value.</summary>
+ public int NewValue { get; }
+
+ /// <summary>The amount by which the scroll value changed.</summary>
+ public int Delta => this.NewValue - this.OldValue;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="position">The cursor position.</param>
+ /// <param name="oldValue">The old scroll value.</param>
+ /// <param name="newValue">The new scroll value.</param>
+ public InputMouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue)
+ {
+ this.Position = position;
+ this.OldValue = oldValue;
+ this.NewValue = newValue;
+ }
+ }
+}
diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs
index ff75c619..e2108de0 100644
--- a/src/SMAPI/Events/LocationEvents.cs
+++ b/src/SMAPI/Events/LocationEvents.cs
@@ -19,22 +19,22 @@ namespace StardewModdingAPI.Events
/// <summary>Raised after a game location is added or removed.</summary>
public static event EventHandler<EventArgsLocationsChanged> LocationsChanged
{
- add => LocationEvents.EventManager.Location_LocationsChanged.Add(value);
- remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value);
+ add => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Add(value);
+ remove => LocationEvents.EventManager.Legacy_Location_LocationsChanged.Remove(value);
}
/// <summary>Raised after buildings are added or removed in a location.</summary>
public static event EventHandler<EventArgsLocationBuildingsChanged> BuildingsChanged
{
- add => LocationEvents.EventManager.Location_BuildingsChanged.Add(value);
- remove => LocationEvents.EventManager.Location_BuildingsChanged.Remove(value);
+ add => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Add(value);
+ remove => LocationEvents.EventManager.Legacy_Location_BuildingsChanged.Remove(value);
}
/// <summary>Raised after objects are added or removed in a location.</summary>
public static event EventHandler<EventArgsLocationObjectsChanged> ObjectsChanged
{
- add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value);
- remove => LocationEvents.EventManager.Location_ObjectsChanged.Remove(value);
+ add => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Add(value);
+ remove => LocationEvents.EventManager.Legacy_Location_ObjectsChanged.Remove(value);
}
diff --git a/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs
new file mode 100644
index 00000000..e73b9396
--- /dev/null
+++ b/src/SMAPI/Events/WorldBuildingListChangedEventArgs.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+using StardewValley.Buildings;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.BuildingListChanged"/> event.</summary>
+ public class WorldBuildingListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The buildings added to the location.</summary>
+ public IEnumerable<Building> Added { get; }
+
+ /// <summary>The buildings removed from the location.</summary>
+ public IEnumerable<Building> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The buildings added to the location.</param>
+ /// <param name="removed">The buildings removed from the location.</param>
+ public WorldBuildingListChangedEventArgs(GameLocation location, IEnumerable<Building> added, IEnumerable<Building> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs
new file mode 100644
index 00000000..053a0e41
--- /dev/null
+++ b/src/SMAPI/Events/WorldLargeTerrainFeatureListChangedEventArgs.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+using StardewValley.TerrainFeatures;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.LargeTerrainFeatureListChanged"/> event.</summary>
+ public class WorldLargeTerrainFeatureListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The large terrain features added to the location.</summary>
+ public IEnumerable<LargeTerrainFeature> Added { get; }
+
+ /// <summary>The large terrain features removed from the location.</summary>
+ public IEnumerable<LargeTerrainFeature> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The large terrain features added to the location.</param>
+ /// <param name="removed">The large terrain features removed from the location.</param>
+ public WorldLargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable<LargeTerrainFeature> added, IEnumerable<LargeTerrainFeature> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs
new file mode 100644
index 00000000..8bc26a43
--- /dev/null
+++ b/src/SMAPI/Events/WorldLocationListChangedEventArgs.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.LocationListChanged"/> event.</summary>
+ public class WorldLocationListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The added locations.</summary>
+ public IEnumerable<GameLocation> Added { get; }
+
+ /// <summary>The removed locations.</summary>
+ public IEnumerable<GameLocation> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="added">The added locations.</param>
+ /// <param name="removed">The removed locations.</param>
+ public WorldLocationListChangedEventArgs(IEnumerable<GameLocation> added, IEnumerable<GameLocation> removed)
+ {
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs
new file mode 100644
index 00000000..e251f894
--- /dev/null
+++ b/src/SMAPI/Events/WorldNpcListChangedEventArgs.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewValley;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.NpcListChanged"/> event.</summary>
+ public class WorldNpcListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The NPCs added to the location.</summary>
+ public IEnumerable<NPC> Added { get; }
+
+ /// <summary>The NPCs removed from the location.</summary>
+ public IEnumerable<NPC> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The NPCs added to the location.</param>
+ /// <param name="removed">The NPCs removed from the location.</param>
+ public WorldNpcListChangedEventArgs(GameLocation location, IEnumerable<NPC> added, IEnumerable<NPC> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs
new file mode 100644
index 00000000..5623a49b
--- /dev/null
+++ b/src/SMAPI/Events/WorldObjectListChangedEventArgs.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using Object = StardewValley.Object;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.ObjectListChanged"/> event.</summary>
+ public class WorldObjectListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The objects added to the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, Object>> Added { get; }
+
+ /// <summary>The objects removed from the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, Object>> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The objects added to the location.</param>
+ /// <param name="removed">The objects removed from the location.</param>
+ public WorldObjectListChangedEventArgs(GameLocation location, IEnumerable<KeyValuePair<Vector2, Object>> added, IEnumerable<KeyValuePair<Vector2, Object>> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs
new file mode 100644
index 00000000..cb089811
--- /dev/null
+++ b/src/SMAPI/Events/WorldTerrainFeatureListChangedEventArgs.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.TerrainFeatures;
+
+namespace StardewModdingAPI.Events
+{
+ /// <summary>Event arguments for a <see cref="IWorldEvents.TerrainFeatureListChanged"/> event.</summary>
+ public class WorldTerrainFeatureListChangedEventArgs : EventArgs
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The location which changed.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>The terrain features added to the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, TerrainFeature>> Added { get; }
+
+ /// <summary>The terrain features removed from the location.</summary>
+ public IEnumerable<KeyValuePair<Vector2, TerrainFeature>> Removed { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The location which changed.</param>
+ /// <param name="added">The terrain features added to the location.</param>
+ /// <param name="removed">The terrain features removed from the location.</param>
+ public WorldTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable<KeyValuePair<Vector2, TerrainFeature>> added, IEnumerable<KeyValuePair<Vector2, TerrainFeature>> removed)
+ {
+ this.Location = location;
+ this.Added = added.ToArray();
+ this.Removed = removed.ToArray();
+ }
+ }
+}
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
});
}
diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs
new file mode 100644
index 00000000..328f504b
--- /dev/null
+++ b/src/SMAPI/IInputHelper.cs
@@ -0,0 +1,21 @@
+namespace StardewModdingAPI
+{
+ /// <summary>Provides an API for checking and changing input state.</summary>
+ public interface IInputHelper : IModLinked
+ {
+ /// <summary>Get the current cursor position.</summary>
+ ICursorPosition GetCursorPosition();
+
+ /// <summary>Get whether a button is currently pressed.</summary>
+ /// <param name="button">The button.</param>
+ bool IsDown(SButton button);
+
+ /// <summary>Get whether a button is currently suppressed, so the game won't see it.</summary>
+ /// <param name="button">The button.</param>
+ bool IsSuppressed(SButton 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>
+ void Suppress(SButton button);
+ }
+}
diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs
index 5e39161d..68c2f1c4 100644
--- a/src/SMAPI/IModHelper.cs
+++ b/src/SMAPI/IModHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using StardewModdingAPI.Events;
namespace StardewModdingAPI
{
@@ -12,6 +13,10 @@ namespace StardewModdingAPI
/// <summary>The full path to the mod's folder.</summary>
string DirectoryPath { get; }
+ /// <summary>Manages access to events raised by SMAPI, which let your mod react when something happens in the game.</summary>
+ [Obsolete("This is an experimental interface which may change at any time. Don't depend on this for released mods.")]
+ IModEvents Events { get; }
+
/// <summary>An API for loading content assets.</summary>
IContentHelper Content { get; }
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index a1cfcf0c..d5f5fdcd 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -840,6 +840,7 @@ namespace StardewModdingAPI
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
IModHelper modHelper;
{
+ IModEvents events = new ModEvents(metadata, this.EventManager);
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
@@ -854,7 +855,7 @@ namespace StardewModdingAPI
return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper);
}
- modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
+ modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
}
// init mod
@@ -936,7 +937,7 @@ namespace StardewModdingAPI
{
string[] warnings = this.GetWarningText(metadata.Warnings).ToArray();
if (warnings.Length == 1)
- this.Monitor.Log($" {metadata.DisplayName}: {warnings[0]}", LogLevel.Warn);
+ this.Monitor.Log($" {metadata.DisplayName} {warnings[0]}", LogLevel.Warn);
else
{
this.Monitor.Log($" {metadata.DisplayName}:", LogLevel.Warn);
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 3e6fdb24..96b3aa5b 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -85,19 +85,37 @@
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
+ <Compile Include="Events\InputMouseWheelScrolledEventArgs.cs" />
+ <Compile Include="Events\InputCursorMovedEventArgs.cs" />
+ <Compile Include="Events\InputButtonReleasedEventArgs.cs" />
+ <Compile Include="Events\InputButtonPressedEventArgs.cs" />
<Compile Include="Events\EventArgsLocationBuildingsChanged.cs" />
+ <Compile Include="Events\IInputEvents.cs" />
+ <Compile Include="Events\IWorldEvents.cs" />
<Compile Include="Events\MultiplayerEvents.cs" />
+ <Compile Include="Events\WorldNpcListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldLargeTerrainFeatureListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldTerrainFeatureListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldBuildingListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldLocationListChangedEventArgs.cs" />
+ <Compile Include="Events\WorldObjectListChangedEventArgs.cs" />
<Compile Include="Framework\ContentManagers\BaseContentManager.cs" />
<Compile Include="Framework\ContentManagers\GameContentManager.cs" />
<Compile Include="Framework\ContentManagers\IContentManager.cs" />
<Compile Include="Framework\ContentManagers\ModContentManager.cs" />
+ <Compile Include="Framework\Events\ModEventsBase.cs" />
<Compile Include="Framework\Events\EventManager.cs" />
+ <Compile Include="Events\IModEvents.cs" />
<Compile Include="Framework\Events\ManagedEvent.cs" />
<Compile Include="Events\SpecialisedEvents.cs" />
<Compile Include="Framework\ContentPack.cs" />
<Compile Include="Framework\Content\ContentCache.cs" />
<Compile Include="Framework\Events\ManagedEventBase.cs" />
+ <Compile Include="Framework\Events\ModEvents.cs" />
+ <Compile Include="Framework\Events\ModInputEvents.cs" />
<Compile Include="Framework\Input\GamePadStateBuilder.cs" />
+ <Compile Include="Framework\ModHelpers\InputHelper.cs" />
+ <Compile Include="IInputHelper.cs" />
<Compile Include="Framework\Input\SInputState.cs" />
<Compile Include="Framework\Input\InputStatus.cs" />
<Compile Include="Framework\LegacyManifestVersion.cs" />
@@ -129,6 +147,7 @@
<Compile Include="Framework\ModLoading\Rewriters\TypeReferenceRewriter.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
+ <Compile Include="Framework\Events\ModWorldEvents.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyFactory.cs" />
<Compile Include="Framework\RewriteFacades\SpriteBatchMethods.cs" />
diff --git a/src/SMAPI/StardewModdingAPI.metadata.json b/src/SMAPI/StardewModdingAPI.metadata.json
index 33568692..be9369f8 100644
--- a/src/SMAPI/StardewModdingAPI.metadata.json
+++ b/src/SMAPI/StardewModdingAPI.metadata.json
@@ -878,8 +878,8 @@
},
"Level Extender": {
- "ID": "Devin Lematty.Level Extender",
- "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated
+ "ID": "DevinLematty.LevelExtender",
+ "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3
"Default | UpdateKey": "Nexus:1471"
},
@@ -1090,7 +1090,8 @@
},
"NPC Map Locations": {
- "ID": "NPCMapLocationsMod",
+ "ID": "Bouhm.NPCMapLocations",
+ "FormerIDs": "NPCMapLocationsMod", // changed in 2.0
"Default | UpdateKey": "Nexus:239"
},
@@ -1403,7 +1404,7 @@
},
"Siv's Marriage Mod": {
- "ID": "6266959802",
+ "FormerIDs": "6266959802 | Siv.MarriageMod | medoli900.Siv's Marriage Mod", // changed in 1.2.3-unofficial versions
"MapLocalVersions": { "0.0": "1.4" },
"Default | UpdateKey": "Nexus:366"
},
@@ -1484,7 +1485,7 @@
"Sprint and Dash": {
"ID": "SPDSprintAndDash",
- "Default | UpdateKey": "Chucklefish:3531",
+ "Default | UpdateKey": "Nexus:235",
"~1.0 | Status": "AssumeBroken" // broke in SDV 1.2
},