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.
This commit is contained in:
Jesse Plamondon-Willard 2018-12-26 20:41:20 -05:00
parent 382b5fe914
commit 678fe27f3f
No known key found for this signature in database
GPG Key ID: 7D7C8097B62033CE
8 changed files with 190 additions and 9 deletions

View File

@ -0,0 +1,56 @@
using System;
using Microsoft.Xna.Framework;
using StardewValley;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IPlayerEvents.CheckedForAction"/> event.</summary>
public class CheckedForActionEventArgs : EventArgs
{
/*********
** Properties
*********/
/// <summary>The underlying field for <see cref="ActionPropertyValue"/>.</summary>
private readonly Lazy<string> ActionPropertyValueImpl;
/*********
** Accessors
*********/
/// <summary>The player who checked for an action.</summary>
public Farmer Player { get; }
/// <summary>The tile checked.</summary>
public Vector2 Tile { get; }
/// <summary>The value of the <c>Action</c> tile property, if any.</summary>
public string ActionPropertyValue => this.ActionPropertyValueImpl.Value;
/// <summary>The current cursor position. This may differ from <see cref="Tile"/>, due to how the game selects the target tile for actions in some cases.</summary>
public ICursorPosition Cursor { get; }
/// <summary>Whether the affected player is the local one.</summary>
public bool IsLocalPlayer => this.Player.IsLocalPlayer;
/// <summary>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).</summary>
public bool WasHandled { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="player">The player for whom the action was checked.</param>
/// <param name="tile">The tile checked.</param>
/// <param name="cursorPosition">The current cursor position.</param>
/// <param name="actionPropertyValue">The value of the <c>Action</c> tile property, if any.</param>
/// <param name="wasHandled">Whether the game performed an action in response to the check.</param>
internal CheckedForActionEventArgs(Farmer player, Vector2 tile, ICursorPosition cursorPosition, Lazy<string> actionPropertyValue, bool wasHandled)
{
this.Player = player;
this.Tile = tile;
this.Cursor = cursorPosition;
this.ActionPropertyValueImpl = actionPropertyValue;
this.WasHandled = wasHandled;
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using Microsoft.Xna.Framework;
using StardewValley;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IPlayerEvents.CheckingForAction"/> event.</summary>
public class CheckingForActionEventArgs : EventArgs
{
/*********
** Properties
*********/
/// <summary>The underlying field for <see cref="ActionPropertyValue"/>.</summary>
private readonly Lazy<string> ActionPropertyValueImpl;
/*********
** Accessors
*********/
/// <summary>The player checking for an action.</summary>
public Farmer Player { get; }
/// <summary>The tile being checked.</summary>
public Vector2 Tile { get; }
/// <summary>The value of the <c>Action</c> tile property, if any.</summary>
public string ActionPropertyValue => this.ActionPropertyValueImpl.Value;
/// <summary>The current cursor position. This may differ from <see cref="Tile"/>, due to how the game selects the target tile for actions in some cases.</summary>
public ICursorPosition Cursor { get; }
/// <summary>Whether the affected player is the local one.</summary>
public bool IsLocalPlayer => this.Player.IsLocalPlayer;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="player">The player for whom the action is being checked.</param>
/// <param name="tile">The tile being checked.</param>
/// <param name="cursorPosition">The current cursor position.</param>
/// <param name="actionPropertyValue">The value of the <c>Action</c> tile property, if any.</param>
internal CheckingForActionEventArgs(Farmer player, Vector2 tile, ICursorPosition cursorPosition, Lazy<string> actionPropertyValue)
{
this.Player = player;
this.Tile = tile;
this.Cursor = cursorPosition;
this.ActionPropertyValueImpl = actionPropertyValue;
}
}
}

View File

@ -5,6 +5,12 @@ namespace StardewModdingAPI.Events
/// <summary>Events raised when the player data changes.</summary> /// <summary>Events raised when the player data changes.</summary>
public interface IPlayerEvents public interface IPlayerEvents
{ {
/// <summary>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.</summary>
event EventHandler<CheckingForActionEventArgs> CheckingForAction;
/// <summary>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.</summary>
event EventHandler<CheckedForActionEventArgs> CheckedForAction;
/// <summary>Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player.</summary> /// <summary>Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player.</summary>
event EventHandler<InventoryChangedEventArgs> InventoryChanged; event EventHandler<InventoryChangedEventArgs> InventoryChanged;

View File

@ -121,6 +121,12 @@ namespace StardewModdingAPI.Framework.Events
/**** /****
** Player ** Player
****/ ****/
/// <summary>Raised before the game checks for an action in response to a player click.</summary>
public readonly ManagedEvent<CheckingForActionEventArgs> CheckingForAction;
/// <summary>Raised after the game checks for an action in response to a player click.</summary>
public readonly ManagedEvent<CheckedForActionEventArgs> CheckedForAction;
/// <summary>Raised after items are added or removed to a player's inventory.</summary> /// <summary>Raised after items are added or removed to a player's inventory.</summary>
public readonly ManagedEvent<InventoryChangedEventArgs> InventoryChanged; public readonly ManagedEvent<InventoryChangedEventArgs> InventoryChanged;
@ -407,6 +413,8 @@ namespace StardewModdingAPI.Framework.Events
this.ModMessageReceived = ManageEventOf<ModMessageReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.ModMessageReceived = ManageEventOf<ModMessageReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived));
this.PeerDisconnected = ManageEventOf<PeerDisconnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); this.PeerDisconnected = ManageEventOf<PeerDisconnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected));
this.CheckingForAction = ManageEventOf<CheckingForActionEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.CheckingForAction));
this.CheckedForAction = ManageEventOf<CheckedForActionEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.CheckedForAction));
this.InventoryChanged = ManageEventOf<InventoryChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); this.InventoryChanged = ManageEventOf<InventoryChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged));
this.LevelChanged = ManageEventOf<LevelChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); this.LevelChanged = ManageEventOf<LevelChangedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged));
this.Warped = ManageEventOf<WarpedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); this.Warped = ManageEventOf<WarpedEventArgs>(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped));

View File

@ -9,6 +9,20 @@ namespace StardewModdingAPI.Framework.Events
/********* /*********
** Accessors ** Accessors
*********/ *********/
/// <summary>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.</summary>
public event EventHandler<CheckingForActionEventArgs> CheckingForAction
{
add => this.EventManager.CheckingForAction.Add(value);
remove => this.EventManager.CheckingForAction.Remove(value);
}
/// <summary>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.</summary>
public event EventHandler<CheckedForActionEventArgs> CheckedForAction
{
add => this.EventManager.CheckedForAction.Add(value);
remove => this.EventManager.CheckedForAction.Remove(value);
}
/// <summary>Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player.</summary> /// <summary>Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player.</summary>
public event EventHandler<InventoryChangedEventArgs> InventoryChanged public event EventHandler<InventoryChangedEventArgs> InventoryChanged
{ {

View File

@ -163,7 +163,7 @@ namespace StardewModdingAPI.Framework
this.OnGameExiting = onGameExiting; this.OnGameExiting = onGameExiting;
Game1.input = new SInputState(); Game1.input = new SInputState();
Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived); 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 // init observables
Game1.locations = new ObservableCollection<GameLocation>(); Game1.locations = new ObservableCollection<GameLocation>();
@ -193,9 +193,38 @@ namespace StardewModdingAPI.Framework
} }
/// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary> /// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary>
protected void OnNewDayAfterFade() /// <param name="resume">Resume the vanilla logic.</param>
protected void OnNewDayAfterFade(Action resume)
{ {
this.Events.DayEnding.RaiseEmpty(); this.Events.DayEnding.RaiseEmpty();
resume();
}
/// <summary>A callback invoked before <see cref="GameLocation.checkAction"/> runs.</summary>
/// <param name="location">The location being checked.</param>
/// <param name="tileLocation">The tile coordinate being checked.</param>
/// <param name="who">The player checking for an action.</param>
/// <param name="resume">Resume the default logic.</param>
private bool OnLocationCheckingAction(GameLocation location, Location tileLocation, Farmer who, Func<bool> 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<string> actionPropertyValue = new Lazy<string>(() => 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;
} }
/// <summary>A callback invoked when a mod message is received.</summary> /// <summary>A callback invoked when a mod message is received.</summary>

View File

@ -1,5 +1,6 @@
using System; using System;
using StardewValley; using StardewValley;
using xTile.Dimensions;
namespace StardewModdingAPI.Framework namespace StardewModdingAPI.Framework
{ {
@ -10,25 +11,39 @@ namespace StardewModdingAPI.Framework
** Properties ** Properties
*********/ *********/
/// <summary>A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</summary> /// <summary>A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</summary>
private readonly Action BeforeNewDayAfterFade; private readonly Action<Action> BeforeNewDayAfterFade;
/// <summary>A callback to invoke before <see cref="GameLocation.checkAction"/> runs.</summary>
private readonly Func<GameLocation, Location, Farmer, Func<bool>, bool> BeforeCheckAction;
/********* /*********
** Public methods ** Public methods
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="beforeNewDayAfterFade">A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</param> /// <param name="beforeNewDayAfterFade">A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</param>
public SModHooks(Action beforeNewDayAfterFade) /// <param name="beforeCheckAction">A callback to invoke before <see cref="GameLocation.checkAction"/> runs.</param>
public SModHooks(Action<Action> beforeNewDayAfterFade, Func<GameLocation, Location, Farmer, Func<bool>, bool> beforeCheckAction)
{ {
this.BeforeNewDayAfterFade = beforeNewDayAfterFade; this.BeforeNewDayAfterFade = beforeNewDayAfterFade;
this.BeforeCheckAction = beforeCheckAction;
} }
/// <summary>A hook invoked when <see cref="Game1.newDayAfterFade"/> is called.</summary> /// <summary>A hook invoked when <see cref="Game1.newDayAfterFade"/> is called.</summary>
/// <param name="action">The vanilla <see cref="Game1.newDayAfterFade"/> logic.</param> /// <param name="resume">Resume the vanilla logic.</param>
public override void OnGame1_NewDayAfterFade(Action action) public override void OnGame1_NewDayAfterFade(Action resume)
{ {
this.BeforeNewDayAfterFade?.Invoke(); this.BeforeNewDayAfterFade(resume);
action(); }
/// <summary>A hook invoked when <see cref="GameLocation.checkAction"/> is called.</summary>
/// <param name="location">The location being checked.</param>
/// <param name="tileLocation">The tile coordinate being checked.</param>
/// <param name="viewport">The current viewport.</param>
/// <param name="who">The player checking for an action.</param>
/// <param name="resume">Resume the default logic.</param>
public override bool OnGameLocation_CheckAction(GameLocation location, Location tileLocation, Rectangle viewport, Farmer who, Func<bool> resume)
{
return this.BeforeCheckAction(location, tileLocation, who, resume);
} }
} }
} }

View File

@ -82,6 +82,8 @@
<Compile Include="Events\ButtonPressedEventArgs.cs" /> <Compile Include="Events\ButtonPressedEventArgs.cs" />
<Compile Include="Events\ButtonReleasedEventArgs.cs" /> <Compile Include="Events\ButtonReleasedEventArgs.cs" />
<Compile Include="Events\ChangeType.cs" /> <Compile Include="Events\ChangeType.cs" />
<Compile Include="Events\CheckedForActionEventArgs.cs" />
<Compile Include="Events\CheckingForActionEventArgs.cs" />
<Compile Include="Events\ContentEvents.cs" /> <Compile Include="Events\ContentEvents.cs" />
<Compile Include="Events\ControlEvents.cs" /> <Compile Include="Events\ControlEvents.cs" />
<Compile Include="Events\CursorMovedEventArgs.cs" /> <Compile Include="Events\CursorMovedEventArgs.cs" />