diff --git a/docs/release-notes.md b/docs/release-notes.md
index 3731157b..d095ce7c 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -12,6 +12,7 @@
* Fixed save backups being empty in rare cases on macOS.
* For mod authors:
+ * Added `DelegatingModHooks` utility for rare cases where a mod needs to override SMAPI's mod hooks in the game directly.
* Updated to Newtonsoft.Json 13.0.2 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.2)) and Pintail 2.2.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#222)).
## 3.18.1
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index c977ad65..1d146d5f 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -258,7 +258,11 @@ namespace StardewModdingAPI.Framework
monitor: this.Monitor,
reflection: this.Reflection,
eventManager: this.EventManager,
- modHooks: new SModHooks(this.OnNewDayAfterFade, this.Monitor),
+ modHooks: new SModHooks(
+ parent: new ModHooks(),
+ beforeNewDayAfterFade: this.OnNewDayAfterFade,
+ monitor: this.Monitor
+ ),
multiplayer: this.Multiplayer,
exitGameImmediately: this.ExitGameImmediately,
@@ -1795,7 +1799,7 @@ namespace StardewModdingAPI.Framework
// call entry method
try
{
- mod.Entry(mod.Helper!);
+ mod.Entry(mod.Helper);
}
catch (Exception ex)
{
@@ -1822,7 +1826,7 @@ namespace StardewModdingAPI.Framework
}
// validate mod doesn't implement both GetApi() and GetApi(mod)
- if (metadata.Api != null && mod.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IModInfo) })!.DeclaringType != typeof(Mod))
+ if (metadata.Api != null && mod.GetType().GetMethod(nameof(Mod.GetApi), new[] { typeof(IModInfo) })!.DeclaringType != typeof(Mod))
metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IModInfo)}), which isn't allowed. The latter will be ignored.", LogLevel.Error);
}
Context.HeuristicModsRunningCode.TryPop(out _);
diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs
index a7736c8b..ac4f242c 100644
--- a/src/SMAPI/Framework/SModHooks.cs
+++ b/src/SMAPI/Framework/SModHooks.cs
@@ -1,11 +1,12 @@
using System;
using System.Threading.Tasks;
+using StardewModdingAPI.Utilities;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// Invokes callbacks for mod hooks provided by the game.
- internal class SModHooks : ModHooks
+ internal class SModHooks : DelegatingModHooks
{
/*********
** Fields
@@ -21,25 +22,24 @@ namespace StardewModdingAPI.Framework
** Public methods
*********/
/// Construct an instance.
+ /// The underlying hooks to call by default.
/// A callback to invoke before runs.
/// Writes messages to the console.
- public SModHooks(Action beforeNewDayAfterFade, IMonitor monitor)
+ public SModHooks(ModHooks parent, Action beforeNewDayAfterFade, IMonitor monitor)
+ : base(parent)
{
this.BeforeNewDayAfterFade = beforeNewDayAfterFade;
this.Monitor = monitor;
}
- /// A hook invoked when is called.
- /// The vanilla logic.
+ ///
public override void OnGame1_NewDayAfterFade(Action action)
{
this.BeforeNewDayAfterFade();
action();
}
- /// Start an asynchronous task for the game.
- /// The task to start.
- /// A unique key which identifies the task.
+ ///
public override Task StartTask(Task task, string id)
{
this.Monitor.Log($"Synchronizing '{id}' task...");
@@ -48,9 +48,7 @@ namespace StardewModdingAPI.Framework
return task;
}
- /// Start an asynchronous task for the game.
- /// The task to start.
- /// A unique key which identifies the task.
+ ///
public override Task StartTask(Task task, string id)
{
this.Monitor.Log($"Synchronizing '{id}' task...");
diff --git a/src/SMAPI/Utilities/DelegatingModHooks.cs b/src/SMAPI/Utilities/DelegatingModHooks.cs
new file mode 100644
index 00000000..3ebcf997
--- /dev/null
+++ b/src/SMAPI/Utilities/DelegatingModHooks.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Xna.Framework.Input;
+using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework;
+using StardewValley;
+using StardewValley.Events;
+
+namespace StardewModdingAPI.Utilities
+{
+ /// An implementation of which automatically calls the parent instance for any method that's not overridden.
+ /// The mod hooks are primarily meant for SMAPI to use. Using this directly in mods is a last resort, since it's very easy to break SMAPI this way. This class requires that SMAPI is present in the parent chain.
+ public class DelegatingModHooks : ModHooks
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The underlying instance to delegate to by default.
+ public ModHooks Parent { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The underlying instance to delegate to by default.
+ public DelegatingModHooks(ModHooks modHooks)
+ {
+ this.AssertSmapiInChain(modHooks);
+
+ this.Parent = modHooks;
+ }
+
+ /// Raised before the in-game clock changes.
+ /// Run the vanilla update logic.
+ /// In mods, consider using instead.
+ public override void OnGame1_PerformTenMinuteClockUpdate(Action action)
+ {
+ this.Parent.OnGame1_PerformTenMinuteClockUpdate(action);
+ }
+
+ /// Raised before initializing the new day and saving.
+ /// Run the vanilla update logic.
+ /// In mods, consider using or instead.
+ public override void OnGame1_NewDayAfterFade(Action action)
+ {
+ this.Parent.OnGame1_NewDayAfterFade(action);
+ }
+
+ /// Raised before showing the end-of-day menus (e.g. shipping menus, level-up screen, etc).
+ /// Run the vanilla update logic.
+ public override void OnGame1_ShowEndOfNightStuff(Action action)
+ {
+ this.Parent.OnGame1_ShowEndOfNightStuff(action);
+ }
+
+ /// Raised before updating the gamepad, mouse, and keyboard input state.
+ /// The keyboard state.
+ /// The mouse state.
+ /// The gamepad state.
+ /// Run the vanilla update logic.
+ /// In mods, consider using instead.
+ public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action)
+ {
+ this.Parent.OnGame1_UpdateControlInput(ref keyboardState, ref mouseState, ref gamePadState, action);
+ }
+
+ /// Raised before a location is updated for the local player entering it.
+ /// The location that will be updated.
+ /// Run the vanilla update logic.
+ /// In mods, consider using instead.
+ public override void OnGameLocation_ResetForPlayerEntry(GameLocation location, Action action)
+ {
+ this.Parent.OnGameLocation_ResetForPlayerEntry(location, action);
+ }
+
+ /// Raised before the game checks for an action to trigger for a player interaction with a tile.
+ /// The location being checked.
+ /// The tile position being checked.
+ /// The game's current position and size within the map, measured in pixels.
+ /// The player interacting with the tile.
+ /// Run the vanilla update logic.
+ /// Returns whether the interaction was handled.
+ public override bool OnGameLocation_CheckAction(GameLocation location, xTile.Dimensions.Location tileLocation, xTile.Dimensions.Rectangle viewport, Farmer who, Func action)
+ {
+ return this.Parent.OnGameLocation_CheckAction(location, tileLocation, viewport, who, action);
+ }
+
+ /// Raised before the game picks a night event to show on the farm after the player sleeps.
+ /// Run the vanilla update logic.
+ /// Returns the selected farm event.
+ public override FarmEvent OnUtility_PickFarmEvent(Func action)
+ {
+ return this.Parent.OnUtility_PickFarmEvent(action);
+ }
+
+ /// Start an asynchronous task for the game.
+ /// The task to start.
+ /// A unique key which identifies the task.
+ public override Task StartTask(Task task, string id)
+ {
+ return this.Parent.StartTask(task, id);
+ }
+
+ /// Start an asynchronous task for the game.
+ /// The type returned by the task when it completes.
+ /// The task to start.
+ /// A unique key which identifies the task.
+ public override Task StartTask(Task task, string id)
+ {
+ return this.Parent.StartTask(task, id);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Assert that SMAPI's mod hook implementation is in the inheritance chain.
+ /// The mod hooks to check.
+ private void AssertSmapiInChain(ModHooks hooks)
+ {
+ // this is SMAPI
+ if (this is SModHooks)
+ return;
+
+ // SMAPI in delegated chain
+ for (ModHooks? cur = hooks; cur != null; cur = (cur as DelegatingModHooks)?.Parent)
+ {
+ if (cur is SModHooks)
+ return;
+ }
+
+ // SMAPI not found
+ throw new InvalidOperationException($"Can't create a {nameof(DelegatingModHooks)} instance without SMAPI's mod hooks in the parent chain.");
+ }
+ }
+}