From 678fe27f3fb1eb504b6ed80c4be1e1d2cf34e197 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Dec 2018 20:41:20 -0500 Subject: [PATCH] add player check-action events (#310) This doesn't work reliably yet, since the game only calls the checkAction hook from the base GameLocation.CheckAction method. --- src/SMAPI/Events/CheckedForActionEventArgs.cs | 56 +++++++++++++++++++ .../Events/CheckingForActionEventArgs.cs | 51 +++++++++++++++++ src/SMAPI/Events/IPlayerEvents.cs | 6 ++ src/SMAPI/Framework/Events/EventManager.cs | 8 +++ src/SMAPI/Framework/Events/ModPlayerEvents.cs | 14 +++++ src/SMAPI/Framework/SGame.cs | 35 +++++++++++- src/SMAPI/Framework/SModHooks.cs | 27 +++++++-- src/SMAPI/StardewModdingAPI.csproj | 2 + 8 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 src/SMAPI/Events/CheckedForActionEventArgs.cs create mode 100644 src/SMAPI/Events/CheckingForActionEventArgs.cs diff --git a/src/SMAPI/Events/CheckedForActionEventArgs.cs b/src/SMAPI/Events/CheckedForActionEventArgs.cs new file mode 100644 index 00000000..953ce458 --- /dev/null +++ b/src/SMAPI/Events/CheckedForActionEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Xna.Framework; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class CheckedForActionEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The underlying field for . + private readonly Lazy ActionPropertyValueImpl; + + /********* + ** Accessors + *********/ + /// The player who checked for an action. + public Farmer Player { get; } + + /// The tile checked. + public Vector2 Tile { get; } + + /// The value of the Action tile property, if any. + public string ActionPropertyValue => this.ActionPropertyValueImpl.Value; + + /// The current cursor position. This may differ from , due to how the game selects the target tile for actions in some cases. + public ICursorPosition Cursor { get; } + + /// Whether the affected player is the local one. + public bool IsLocalPlayer => this.Player.IsLocalPlayer; + + /// Whether the game performed an action in response to the check. Note that the game sometimes handles input without marking it handled (e.g. when activating a TV or fireplace). + public bool WasHandled { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player for whom the action was checked. + /// The tile checked. + /// The current cursor position. + /// The value of the Action tile property, if any. + /// Whether the game performed an action in response to the check. + internal CheckedForActionEventArgs(Farmer player, Vector2 tile, ICursorPosition cursorPosition, Lazy actionPropertyValue, bool wasHandled) + { + this.Player = player; + this.Tile = tile; + this.Cursor = cursorPosition; + this.ActionPropertyValueImpl = actionPropertyValue; + this.WasHandled = wasHandled; + } + } +} diff --git a/src/SMAPI/Events/CheckingForActionEventArgs.cs b/src/SMAPI/Events/CheckingForActionEventArgs.cs new file mode 100644 index 00000000..3857aae4 --- /dev/null +++ b/src/SMAPI/Events/CheckingForActionEventArgs.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Xna.Framework; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class CheckingForActionEventArgs : EventArgs + { + /********* + ** Properties + *********/ + /// The underlying field for . + private readonly Lazy ActionPropertyValueImpl; + + /********* + ** Accessors + *********/ + /// The player checking for an action. + public Farmer Player { get; } + + /// The tile being checked. + public Vector2 Tile { get; } + + /// The value of the Action tile property, if any. + public string ActionPropertyValue => this.ActionPropertyValueImpl.Value; + + /// The current cursor position. This may differ from , due to how the game selects the target tile for actions in some cases. + public ICursorPosition Cursor { get; } + + /// Whether the affected player is the local one. + public bool IsLocalPlayer => this.Player.IsLocalPlayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player for whom the action is being checked. + /// The tile being checked. + /// The current cursor position. + /// The value of the Action tile property, if any. + internal CheckingForActionEventArgs(Farmer player, Vector2 tile, ICursorPosition cursorPosition, Lazy actionPropertyValue) + { + this.Player = player; + this.Tile = tile; + this.Cursor = cursorPosition; + this.ActionPropertyValueImpl = actionPropertyValue; + } + } +} diff --git a/src/SMAPI/Events/IPlayerEvents.cs b/src/SMAPI/Events/IPlayerEvents.cs index 81e17b1a..a221e40d 100644 --- a/src/SMAPI/Events/IPlayerEvents.cs +++ b/src/SMAPI/Events/IPlayerEvents.cs @@ -5,6 +5,12 @@ namespace StardewModdingAPI.Events /// Events raised when the player data changes. public interface IPlayerEvents { + /// Raised before the game checks for an action in response to a player input. That includes activating an interactive object, opening a chest, putting an item in a machine, etc. NOTE: this event is currently only raised for the current player. + event EventHandler CheckingForAction; + + /// Raised after the game checks for an action in response to a player input. That includes activating an interactive object, opening a chest, putting an item in a machine, etc. NOTE: this event is currently only raised for the current player. + event EventHandler CheckedForAction; + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player. event EventHandler InventoryChanged; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 13244601..72899229 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -121,6 +121,12 @@ namespace StardewModdingAPI.Framework.Events /**** ** Player ****/ + /// Raised before the game checks for an action in response to a player click. + public readonly ManagedEvent CheckingForAction; + + /// Raised after the game checks for an action in response to a player click. + public readonly ManagedEvent CheckedForAction; + /// Raised after items are added or removed to a player's inventory. public readonly ManagedEvent InventoryChanged; @@ -407,6 +413,8 @@ namespace StardewModdingAPI.Framework.Events this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.PeerDisconnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); + this.CheckingForAction = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.CheckingForAction)); + this.CheckedForAction = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.CheckedForAction)); this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); this.Warped = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); diff --git a/src/SMAPI/Framework/Events/ModPlayerEvents.cs b/src/SMAPI/Framework/Events/ModPlayerEvents.cs index ca7cfd96..056127c4 100644 --- a/src/SMAPI/Framework/Events/ModPlayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModPlayerEvents.cs @@ -9,6 +9,20 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Raised before the game checks for an action in response to a player click. That includes activating an interactive object, opening a chest, talking to an NPC, putting an item in a machine, etc. NOTE: this event is currently only raised for the current player. + public event EventHandler CheckingForAction + { + add => this.EventManager.CheckingForAction.Add(value); + remove => this.EventManager.CheckingForAction.Remove(value); + } + + /// Raised after the game checks for an action in response to a player input. That includes activating an interactive object, opening a chest, putting an item in a machine, etc. NOTE: this event is currently only raised for the current player. + public event EventHandler CheckedForAction + { + add => this.EventManager.CheckedForAction.Add(value); + remove => this.EventManager.CheckedForAction.Remove(value); + } + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player. public event EventHandler InventoryChanged { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 957fd1d6..f84080d3 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -163,7 +163,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived); - Game1.hooks = new SModHooks(this.OnNewDayAfterFade); + Game1.hooks = new SModHooks(this.OnNewDayAfterFade, this.OnLocationCheckingAction); // init observables Game1.locations = new ObservableCollection(); @@ -193,9 +193,38 @@ namespace StardewModdingAPI.Framework } /// A callback invoked before runs. - protected void OnNewDayAfterFade() + /// Resume the vanilla logic. + protected void OnNewDayAfterFade(Action resume) { this.Events.DayEnding.RaiseEmpty(); + resume(); + } + + /// A callback invoked before runs. + /// The location being checked. + /// The tile coordinate being checked. + /// The player checking for an action. + /// Resume the default logic. + private bool OnLocationCheckingAction(GameLocation location, Location tileLocation, Farmer who, Func resume) + { + // check for event listeners + bool hasPreListeners = this.Events.CheckingForAction.HasListeners(); + bool hasPostListeners = this.Events.CheckedForAction.HasListeners(); + if (!hasPreListeners && !hasPostListeners) + return resume(); + + // get tile info + Vector2 tilePos = new Vector2(tileLocation.X, tileLocation.Y); + Lazy actionPropertyValue = new Lazy(() => location.doesTileHaveProperty(tileLocation.X, tileLocation.Y, "Action", "Buildings")); + + // raise events + if (hasPreListeners) + this.Events.CheckingForAction.Raise(new CheckingForActionEventArgs(who, tilePos, this.Input.CursorPosition, actionPropertyValue)); + bool result = resume(); + if (hasPostListeners) + this.Events.CheckedForAction.Raise(new CheckedForActionEventArgs(who, tilePos, this.Input.CursorPosition, actionPropertyValue, result)); + + return result; } /// A callback invoked when a mod message is received. @@ -1384,7 +1413,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } - label_129: + label_129: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs index 9f0201c8..6abd9898 100644 --- a/src/SMAPI/Framework/SModHooks.cs +++ b/src/SMAPI/Framework/SModHooks.cs @@ -1,5 +1,6 @@ using System; using StardewValley; +using xTile.Dimensions; namespace StardewModdingAPI.Framework { @@ -10,25 +11,39 @@ namespace StardewModdingAPI.Framework ** Properties *********/ /// A callback to invoke before runs. - private readonly Action BeforeNewDayAfterFade; + private readonly Action BeforeNewDayAfterFade; + /// A callback to invoke before runs. + private readonly Func, bool> BeforeCheckAction; /********* ** Public methods *********/ /// Construct an instance. /// A callback to invoke before runs. - public SModHooks(Action beforeNewDayAfterFade) + /// A callback to invoke before runs. + public SModHooks(Action beforeNewDayAfterFade, Func, bool> beforeCheckAction) { this.BeforeNewDayAfterFade = beforeNewDayAfterFade; + this.BeforeCheckAction = beforeCheckAction; } /// A hook invoked when is called. - /// The vanilla logic. - public override void OnGame1_NewDayAfterFade(Action action) + /// Resume the vanilla logic. + public override void OnGame1_NewDayAfterFade(Action resume) { - this.BeforeNewDayAfterFade?.Invoke(); - action(); + this.BeforeNewDayAfterFade(resume); + } + + /// A hook invoked when is called. + /// The location being checked. + /// The tile coordinate being checked. + /// The current viewport. + /// The player checking for an action. + /// Resume the default logic. + public override bool OnGameLocation_CheckAction(GameLocation location, Location tileLocation, Rectangle viewport, Farmer who, Func resume) + { + return this.BeforeCheckAction(location, tileLocation, who, resume); } } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5540f277..69660914 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -82,6 +82,8 @@ + +