From 902814d308289f169750a615ae573edc348893d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 15:05:08 -0400 Subject: don't send chatbox input to mods (#453) --- src/SMAPI/Framework/Input/InputState.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/SMAPI/Framework/Input') diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs index 8b0108ae..7c8676e9 100644 --- a/src/SMAPI/Framework/Input/InputState.cs +++ b/src/SMAPI/Framework/Input/InputState.cs @@ -72,6 +72,13 @@ namespace StardewModdingAPI.Framework.Input return this.GetStatus(button).IsDown(); } + /// Get whether any of the given buttons were pressed or held. + /// The buttons to check. + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + /// Get the current input state. /// The previous input state. public static InputState GetState(InputState previousState) -- cgit From 5e7eaf9f75d72d8cbb338c35b43f2974440b3456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Apr 2018 19:59:03 -0400 Subject: rewrite input suppression (#453) This lets SMAPI intercept all input using the new Game1.hooks in SDV 1.3.0.32. However, intercepting mouse clicks needs a few more changes in the game code. --- docs/release-notes.md | 3 +- src/SMAPI/Events/EventArgsInput.cs | 109 ++-------------- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 153 +++++++++++++++++++++++ src/SMAPI/Framework/Input/InputState.cs | 78 +++++++++--- src/SMAPI/Framework/SGame.cs | 78 +++++++++++- src/SMAPI/SModHooks.cs | 48 +++++++ src/SMAPI/StardewModdingAPI.csproj | 2 + 7 files changed, 357 insertions(+), 114 deletions(-) create mode 100644 src/SMAPI/Framework/Input/GamePadStateBuilder.cs create mode 100644 src/SMAPI/SModHooks.cs (limited to 'src/SMAPI/Framework/Input') diff --git a/docs/release-notes.md b/docs/release-notes.md index ee2d0aa1..565ed58c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,7 +22,8 @@ * Added prerelease versions to the mod update-check API response where available (GitHub only). * Added support for beta releases on the home page. * Split mod DB out of `StardewModdingAPI.config.json`, so we can load config earlier and reduce unnecessary memory usage later. - * Overhauled world/player state tracking: + * Rewrote input suppression using new SDV 1.3 APIs. + * Rewrote world/player state tracking: * much more efficient than previous method; * uses net field events where available; * lays groundwork for tracking events for multiple players. diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 0cf0828b..d60f4017 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -1,14 +1,18 @@ using System; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewValley; +using System.Collections.Generic; namespace StardewModdingAPI.Events { /// Event arguments when a button is pressed or released. public class EventArgsInput : EventArgs { + /********* + ** Properties + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + /********* ** Accessors *********/ @@ -25,7 +29,7 @@ namespace StardewModdingAPI.Events public bool IsUseToolButton { get; } /// Whether a mod has indicated the key was already handled. - public bool IsSuppressed { get; private set; } + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); /********* @@ -36,12 +40,14 @@ namespace StardewModdingAPI.Events /// The cursor position. /// Whether the input should trigger actions on the affected tile. /// Whether the input should use tools on the affected tile. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) + /// The buttons to suppress. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton, HashSet suppressButtons) { this.Button = button; this.Cursor = cursor; this.IsActionButton = isActionButton; this.IsUseToolButton = isUseToolButton; + this.SuppressButtons = suppressButtons; } /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. @@ -54,96 +60,7 @@ namespace StardewModdingAPI.Events /// The button to suppress. public void SuppressButton(SButton button) { - if (button == this.Button) - this.IsSuppressed = true; - - // keyboard - if (button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); - - // controller - else if (button.TryGetController(out Buttons controllerButton)) - { - var newState = GamePad.GetState(PlayerIndex.One); - var thumbsticks = Game1.oldPadState.ThumbSticks; - var triggers = Game1.oldPadState.Triggers; - var buttons = Game1.oldPadState.Buttons; - var dpad = Game1.oldPadState.DPad; - - switch (controllerButton) - { - // d-pad - case Buttons.DPadDown: - dpad = new GamePadDPad(dpad.Up, newState.DPad.Down, dpad.Left, dpad.Right); - break; - case Buttons.DPadLeft: - dpad = new GamePadDPad(dpad.Up, dpad.Down, newState.DPad.Left, dpad.Right); - break; - case Buttons.DPadRight: - dpad = new GamePadDPad(dpad.Up, dpad.Down, dpad.Left, newState.DPad.Right); - break; - case Buttons.DPadUp: - dpad = new GamePadDPad(newState.DPad.Up, dpad.Down, dpad.Left, dpad.Right); - break; - - // trigger - case Buttons.LeftTrigger: - triggers = new GamePadTriggers(newState.Triggers.Left, triggers.Right); - break; - case Buttons.RightTrigger: - triggers = new GamePadTriggers(triggers.Left, newState.Triggers.Right); - break; - - // thumbstick - case Buttons.LeftThumbstickDown: - case Buttons.LeftThumbstickLeft: - case Buttons.LeftThumbstickRight: - case Buttons.LeftThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Left, thumbsticks.Right); - break; - case Buttons.RightThumbstickDown: - case Buttons.RightThumbstickLeft: - case Buttons.RightThumbstickRight: - case Buttons.RightThumbstickUp: - thumbsticks = new GamePadThumbSticks(newState.ThumbSticks.Right, thumbsticks.Left); - break; - - // buttons - default: - var mask = - (buttons.A == ButtonState.Pressed ? Buttons.A : 0) - | (buttons.B == ButtonState.Pressed ? Buttons.B : 0) - | (buttons.Back == ButtonState.Pressed ? Buttons.Back : 0) - | (buttons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0) - | (buttons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0) - | (buttons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0) - | (buttons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0) - | (buttons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0) - | (buttons.Start == ButtonState.Pressed ? Buttons.Start : 0) - | (buttons.X == ButtonState.Pressed ? Buttons.X : 0) - | (buttons.Y == ButtonState.Pressed ? Buttons.Y : 0); - mask = mask ^ controllerButton; - buttons = new GamePadButtons(mask); - break; - } - - Game1.oldPadState = new GamePadState(thumbsticks, triggers, buttons, dpad); - } - - // mouse - else if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - { - Game1.oldMouseState = new MouseState( - x: Game1.oldMouseState.X, - y: Game1.oldMouseState.Y, - scrollWheel: Game1.oldMouseState.ScrollWheelValue, - leftButton: button == SButton.MouseLeft ? ButtonState.Pressed : Game1.oldMouseState.LeftButton, - middleButton: button == SButton.MouseMiddle ? ButtonState.Pressed : Game1.oldMouseState.MiddleButton, - rightButton: button == SButton.MouseRight ? ButtonState.Pressed : Game1.oldMouseState.RightButton, - xButton1: button == SButton.MouseX1 ? ButtonState.Pressed : Game1.oldMouseState.XButton1, - xButton2: button == SButton.MouseX2 ? ButtonState.Pressed : Game1.oldMouseState.XButton2 - ); - } + this.SuppressButtons.Add(button); } } } diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs new file mode 100644 index 00000000..5eeb7ef6 --- /dev/null +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Framework.Input +{ + /// An abstraction for manipulating controller state. + internal class GamePadStateBuilder + { + /********* + ** Properties + *********/ + /// The current button states. + private readonly IDictionary ButtonStates; + + /// The left trigger value. + private float LeftTrigger; + + /// The right trigger value. + private float RightTrigger; + + /// The left thumbstick position. + private Vector2 LeftStickPos; + + /// The left thumbstick position. + private Vector2 RightStickPos; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial controller state. + public GamePadStateBuilder(GamePadState state) + { + this.ButtonStates = new Dictionary + { + [SButton.DPadUp] = state.DPad.Up, + [SButton.DPadDown] = state.DPad.Down, + [SButton.DPadLeft] = state.DPad.Left, + [SButton.DPadRight] = state.DPad.Right, + + [SButton.ControllerA] = state.Buttons.A, + [SButton.ControllerB] = state.Buttons.B, + [SButton.ControllerX] = state.Buttons.X, + [SButton.ControllerY] = state.Buttons.Y, + [SButton.LeftStick] = state.Buttons.LeftStick, + [SButton.RightStick] = state.Buttons.RightStick, + [SButton.LeftShoulder] = state.Buttons.LeftShoulder, + [SButton.RightShoulder] = state.Buttons.RightShoulder, + [SButton.ControllerBack] = state.Buttons.Back, + [SButton.ControllerStart] = state.Buttons.Start, + [SButton.BigButton] = state.Buttons.BigButton + }; + this.LeftTrigger = state.Triggers.Left; + this.RightTrigger = state.Triggers.Right; + this.LeftStickPos = state.ThumbSticks.Left; + this.RightStickPos = state.ThumbSticks.Right; + } + + /// Mark all matching buttons unpressed. + /// The buttons. + public void SuppressButtons(IEnumerable buttons) + { + foreach (SButton button in buttons) + this.SuppressButton(button); + } + + /// Mark a button unpressed. + /// The button. + public void SuppressButton(SButton button) + { + switch (button) + { + // left thumbstick + case SButton.LeftThumbstickUp: + if (this.LeftStickPos.Y > 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickDown: + if (this.LeftStickPos.Y < 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickLeft: + if (this.LeftStickPos.X < 0) + this.LeftStickPos.X = 0; + break; + case SButton.LeftThumbstickRight: + if (this.LeftStickPos.X > 0) + this.LeftStickPos.X = 0; + break; + + // right thumbstick + case SButton.RightThumbstickUp: + if (this.RightStickPos.Y > 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickDown: + if (this.RightStickPos.Y < 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickLeft: + if (this.RightStickPos.X < 0) + this.RightStickPos.X = 0; + break; + case SButton.RightThumbstickRight: + if (this.RightStickPos.X > 0) + this.RightStickPos.X = 0; + break; + + // triggers + case SButton.LeftTrigger: + this.LeftTrigger = 0; + break; + case SButton.RightTrigger: + this.RightTrigger = 0; + break; + + // buttons + default: + if (this.ButtonStates.ContainsKey(button)) + this.ButtonStates[button] = ButtonState.Released; + break; + } + } + + /// Construct an equivalent gamepad state. + public GamePadState ToGamePadState() + { + return new GamePadState( + leftThumbStick: this.LeftStickPos, + rightThumbStick: this.RightStickPos, + leftTrigger: this.LeftTrigger, + rightTrigger: this.RightTrigger, + buttons: this.GetPressedButtons().ToArray() + ); + } + + /********* + ** Private methods + *********/ + /// Get all pressed buttons. + private IEnumerable GetPressedButtons() + { + foreach (var pair in this.ButtonStates) + { + if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button)) + yield return button; + } + } + } +} diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs index 7c8676e9..62337a6c 100644 --- a/src/SMAPI/Framework/Input/InputState.cs +++ b/src/SMAPI/Framework/Input/InputState.cs @@ -9,6 +9,16 @@ namespace StardewModdingAPI.Framework.Input /// A summary of input changes during an update frame. internal class InputState { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + /// The maximum amount of direction to ignore for the right thumbstick. + private const float RightThumbstickDeadZone = 0f; + + /********* ** Accessors *********/ @@ -48,7 +58,7 @@ namespace StardewModdingAPI.Framework.Input this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX // get button states - SButton[] down = InputState.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); + SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); foreach (SButton button in down) this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true); foreach (KeyValuePair prev in previousState.ActiveButtons) @@ -109,7 +119,8 @@ namespace StardewModdingAPI.Framework.Input /// The keyboard state. /// The mouse state. /// The controller state. - private static IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) { // keyboard foreach (Keys key in keyboard.GetPressedKeys()) @@ -130,28 +141,23 @@ namespace StardewModdingAPI.Framework.Input // controller if (controller.IsConnected) { + // main buttons if (controller.Buttons.A == ButtonState.Pressed) yield return SButton.ControllerA; if (controller.Buttons.B == ButtonState.Pressed) yield return SButton.ControllerB; - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; if (controller.Buttons.LeftStick == ButtonState.Pressed) yield return SButton.LeftStick; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; if (controller.Buttons.RightStick == ButtonState.Pressed) yield return SButton.RightStick; if (controller.Buttons.Start == ButtonState.Pressed) yield return SButton.ControllerStart; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; + + // directional pad if (controller.DPad.Up == ButtonState.Pressed) yield return SButton.DPadUp; if (controller.DPad.Down == ButtonState.Pressed) @@ -160,11 +166,55 @@ namespace StardewModdingAPI.Framework.Input yield return SButton.DPadLeft; if (controller.DPad.Right == ButtonState.Pressed) yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers if (controller.Triggers.Left > 0.2f) yield return SButton.LeftTrigger; if (controller.Triggers.Right > 0.2f) yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -InputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } } } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 35e027d8..3b9a159f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -120,6 +120,9 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + private readonly HashSet SuppressButtons = new HashSet(); + /********* ** Accessors @@ -154,6 +157,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); + Game1.hooks = new SModHooks(this.UpdateControlInput); // init watchers Game1.locations = new ObservableCollection(); @@ -420,7 +424,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -438,7 +442,7 @@ namespace StardewModdingAPI.Framework } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -539,7 +543,7 @@ namespace StardewModdingAPI.Framework if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher _)) { if (this.VerboseLogging) - this.Monitor.Log($"Context: current location objects changed.", LogLevel.Trace); + this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); } @@ -619,6 +623,17 @@ namespace StardewModdingAPI.Framework } } + /// Read the current input state for handling. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic) + { + this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState); + defaultLogic(); + } + /// The method called to draw everything to the screen. /// A snapshot of the game timing state. protected override void Draw(GameTime gameTime) @@ -1240,6 +1255,63 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ + /// Apply input suppression for the given input states. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + // stop suppressing buttons once released + if (this.SuppressButtons.Count != 0) + { + InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState); + this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p)); + } + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet keyboardButtons = new HashSet(); + HashSet controllerButtons = new HashSet(); + HashSet mouseButtons = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + mouseButtons.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + keyboardButtons.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + controllerButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && controllerButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(controllerButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (mouseButtons.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, + middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, + rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, + xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, + xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + ); + } + } + /// Perform any cleanup needed when the player unloads a save and returns to the title screen. private void CleanupAfterReturnToTitle() { diff --git a/src/SMAPI/SModHooks.cs b/src/SMAPI/SModHooks.cs new file mode 100644 index 00000000..1b88d606 --- /dev/null +++ b/src/SMAPI/SModHooks.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Intercepts predefined Stardew Valley mod hooks. + internal class SModHooks : ModHooks + { + /********* + ** Delegates + *********/ + /// A delegate invoked by the hook. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public delegate void UpdateControlInputDelegate(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action); + + + /********* + ** Properties + *********/ + /// The callback for . + private readonly UpdateControlInputDelegate UpdateControlInputHandler; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The callback for . + public SModHooks(UpdateControlInputDelegate updateControlInputHandler) + { + this.UpdateControlInputHandler = updateControlInputHandler; + } + + /// A hook invoked before the game processes player input. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + /// The game's default logic. + public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action) + { + this.UpdateControlInputHandler(ref keyboardState, ref mouseState, ref gamePadState, action); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5fe3e32c..d7719e27 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ + @@ -262,6 +263,7 @@ + -- cgit From 3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 02:47:07 -0400 Subject: rewrite input suppression again (#453) This uses the new Game1.input in SDV 1.3.0.37 to override the game's input more consistently, though it still doesn't intercept clicks correctly yet. --- src/SMAPI/Framework/Input/InputState.cs | 220 ------------------- src/SMAPI/Framework/Input/SInputState.cs | 359 +++++++++++++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 113 ++-------- src/SMAPI/SModHooks.cs | 48 ----- src/SMAPI/StardewModdingAPI.csproj | 3 +- 5 files changed, 376 insertions(+), 367 deletions(-) delete mode 100644 src/SMAPI/Framework/Input/InputState.cs create mode 100644 src/SMAPI/Framework/Input/SInputState.cs delete mode 100644 src/SMAPI/SModHooks.cs (limited to 'src/SMAPI/Framework/Input') diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs deleted file mode 100644 index 62337a6c..00000000 --- a/src/SMAPI/Framework/Input/InputState.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI.Framework.Input -{ - /// A summary of input changes during an update frame. - internal class InputState - { - /********* - ** Accessors - *********/ - /// The maximum amount of direction to ignore for the left thumbstick. - private const float LeftThumbstickDeadZone = 0.2f; - - /// The maximum amount of direction to ignore for the right thumbstick. - private const float RightThumbstickDeadZone = 0f; - - - /********* - ** Accessors - *********/ - /// The underlying controller state. - public GamePadState ControllerState { get; } - - /// The underlying keyboard state. - public KeyboardState KeyboardState { get; } - - /// The underlying mouse state. - public MouseState MouseState { get; } - - /// The mouse position on the screen adjusted for the zoom level. - public Point MousePosition { get; } - - /// The buttons which were pressed, held, or released. - public IDictionary ActiveButtons { get; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public InputState() { } - - /// Construct an instance. - /// The previous input state. - /// The current controller state. - /// The current keyboard state. - /// The current mouse state. - public InputState(InputState previousState, GamePadState controllerState, KeyboardState keyboardState, MouseState mouseState) - { - // init properties - this.ControllerState = controllerState; - this.KeyboardState = keyboardState; - this.MouseState = mouseState; - this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX - - // get button states - SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray(); - foreach (SButton button in down) - this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true); - foreach (KeyValuePair prev in previousState.ActiveButtons) - { - if (prev.Value.IsDown() && !this.ActiveButtons.ContainsKey(prev.Key)) - this.ActiveButtons[prev.Key] = InputStatus.Released; - } - } - - /// Get the status of a button. - /// The button to check. - public InputStatus GetStatus(SButton button) - { - return this.ActiveButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; - } - - /// Get whether a given button was pressed or held. - /// The button to check. - public bool IsDown(SButton button) - { - return this.GetStatus(button).IsDown(); - } - - /// Get whether any of the given buttons were pressed or held. - /// The buttons to check. - public bool IsAnyDown(InputButton[] buttons) - { - return buttons.Any(button => this.IsDown(button.ToSButton())); - } - - /// Get the current input state. - /// The previous input state. - public static InputState GetState(InputState previousState) - { - GamePadState controllerState = GamePad.GetState(PlayerIndex.One); - KeyboardState keyboardState = Keyboard.GetState(); - MouseState mouseState = Mouse.GetState(); - - return new InputState(previousState, controllerState, keyboardState, mouseState); - } - - /********* - ** Private methods - *********/ - /// Get the status of a button. - /// The previous button status. - /// Whether the button is currently down. - public InputStatus GetStatus(InputStatus oldStatus, bool isDown) - { - if (isDown && oldStatus.IsDown()) - return InputStatus.Held; - if (isDown) - return InputStatus.Pressed; - return InputStatus.Released; - } - - /// Get the buttons pressed in the given stats. - /// The keyboard state. - /// The mouse state. - /// The controller state. - /// Thumbstick direction logic derived from . - private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) - { - // keyboard - foreach (Keys key in keyboard.GetPressedKeys()) - yield return key.ToSButton(); - - // mouse - if (mouse.LeftButton == ButtonState.Pressed) - yield return SButton.MouseLeft; - if (mouse.RightButton == ButtonState.Pressed) - yield return SButton.MouseRight; - if (mouse.MiddleButton == ButtonState.Pressed) - yield return SButton.MouseMiddle; - if (mouse.XButton1 == ButtonState.Pressed) - yield return SButton.MouseX1; - if (mouse.XButton2 == ButtonState.Pressed) - yield return SButton.MouseX2; - - // controller - if (controller.IsConnected) - { - // main buttons - if (controller.Buttons.A == ButtonState.Pressed) - yield return SButton.ControllerA; - if (controller.Buttons.B == ButtonState.Pressed) - yield return SButton.ControllerB; - if (controller.Buttons.X == ButtonState.Pressed) - yield return SButton.ControllerX; - if (controller.Buttons.Y == ButtonState.Pressed) - yield return SButton.ControllerY; - if (controller.Buttons.LeftStick == ButtonState.Pressed) - yield return SButton.LeftStick; - if (controller.Buttons.RightStick == ButtonState.Pressed) - yield return SButton.RightStick; - if (controller.Buttons.Start == ButtonState.Pressed) - yield return SButton.ControllerStart; - - // directional pad - if (controller.DPad.Up == ButtonState.Pressed) - yield return SButton.DPadUp; - if (controller.DPad.Down == ButtonState.Pressed) - yield return SButton.DPadDown; - if (controller.DPad.Left == ButtonState.Pressed) - yield return SButton.DPadLeft; - if (controller.DPad.Right == ButtonState.Pressed) - yield return SButton.DPadRight; - - // secondary buttons - if (controller.Buttons.Back == ButtonState.Pressed) - yield return SButton.ControllerBack; - if (controller.Buttons.BigButton == ButtonState.Pressed) - yield return SButton.BigButton; - - // shoulders - if (controller.Buttons.LeftShoulder == ButtonState.Pressed) - yield return SButton.LeftShoulder; - if (controller.Buttons.RightShoulder == ButtonState.Pressed) - yield return SButton.RightShoulder; - - // triggers - if (controller.Triggers.Left > 0.2f) - yield return SButton.LeftTrigger; - if (controller.Triggers.Right > 0.2f) - yield return SButton.RightTrigger; - - // left thumbstick direction - if (controller.ThumbSticks.Left.Y > InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickUp; - if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickDown; - if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickRight; - if (controller.ThumbSticks.Left.X < -InputState.LeftThumbstickDeadZone) - yield return SButton.LeftThumbstickLeft; - - // right thumbstick direction - if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) - { - if (controller.ThumbSticks.Right.Y > 0) - yield return SButton.RightThumbstickUp; - if (controller.ThumbSticks.Right.Y < 0) - yield return SButton.RightThumbstickDown; - if (controller.ThumbSticks.Right.X > 0) - yield return SButton.RightThumbstickRight; - if (controller.ThumbSticks.Right.X < 0) - yield return SButton.RightThumbstickLeft; - } - } - } - - /// Get whether the right thumbstick should be considered outside the dead zone. - /// The right thumbstick value. - private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) - { - return direction.Length() > 0.9f; - } - } -} diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs new file mode 100644 index 00000000..62defa9f --- /dev/null +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +#pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) +namespace StardewModdingAPI.Framework.Input +{ + /// A summary of input changes during an update frame. + internal sealed class SInputState : InputState + { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + + /********* + ** Accessors + *********/ + /// The controller state as of the last update. + public GamePadState RealController { get; private set; } + + /// The keyboard state as of the last update. + public KeyboardState RealKeyboard { get; private set; } + + /// The mouse state as of the last update. + public MouseState RealMouse { get; private set; } + + /// A derivative of which suppresses the buttons in . + public GamePadState SuppressedController { get; private set; } + + /// A derivative of which suppresses the buttons in . + public KeyboardState SuppressedKeyboard { get; private set; } + + /// A derivative of which suppresses the buttons in . + public MouseState SuppressedMouse { get; private set; } + + /// The mouse position on the screen adjusted for the zoom level. + public Point MousePosition { get; private set; } + + /// The buttons which were pressed, held, or released. + public IDictionary ActiveButtons { get; private set; } = new Dictionary(); + + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + public HashSet SuppressButtons { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Get a copy of the current state. + public SInputState Clone() + { + return new SInputState + { + ActiveButtons = this.ActiveButtons, + RealController = this.RealController, + RealKeyboard = this.RealKeyboard, + RealMouse = this.RealMouse, + MousePosition = this.MousePosition + }; + } + + /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. + [Obsolete("This method should only be called by the game itself.")] + public override void Update() { } + + /// Update the current button statuses for the given tick. + public void TrueUpdate() + { + try + { + // get new states + 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); + + // get suppressed states + GamePadState suppressedController = realController; + KeyboardState suppressedKeyboard = realKeyboard; + MouseState suppressedMouse = realMouse; + if (this.SuppressButtons.Count > 0) + this.UpdateSuppression(activeButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + + // update + this.ActiveButtons = activeButtons; + this.RealController = realController; + this.RealKeyboard = realKeyboard; + this.RealMouse = realMouse; + this.SuppressedController = suppressedController; + this.SuppressedKeyboard = suppressedKeyboard; + this.SuppressedMouse = suppressedMouse; + this.MousePosition = mousePosition; + } + catch (InvalidOperationException) + { + // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + } + } + + /// Get the gamepad state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override GamePadState GetGamePadState() + { + return this.ShouldSuppressNow() + ? this.SuppressedController + : this.RealController; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override KeyboardState GetKeyboardState() + { + return this.ShouldSuppressNow() + ? this.SuppressedKeyboard + : this.RealKeyboard; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override MouseState GetMouseState() + { + return this.ShouldSuppressNow() + ? this.SuppressedMouse + : this.RealMouse; + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.GetStatus(this.ActiveButtons, button).IsDown(); + } + + /// Get whether any of the given buttons were pressed or held. + /// The buttons to check. + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + + /// Apply input suppression for the given input states. + /// The current button states to check. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + public void UpdateSuppression(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + // stop suppressing buttons once released + if (this.SuppressButtons.Count != 0) + this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet keyboardButtons = new HashSet(); + HashSet controllerButtons = new HashSet(); + HashSet mouseButtons = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + mouseButtons.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + keyboardButtons.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + controllerButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && controllerButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(controllerButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (mouseButtons.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, + middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, + rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, + xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, + xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + ); + } + } + + + /********* + ** Private methods + *********/ + /// Whether input should be suppressed in the current context. + private bool ShouldSuppressNow() + { + return Game1.chatBox != null && !Game1.chatBox.isActive(); + } + + /// Get the status of all pressed or released buttons relative to their previous status. + /// The previous button statuses. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IDictionary DeriveStatuses(IDictionary previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + IDictionary activeButtons = new Dictionary(); + + // handle pressed keys + SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray(); + foreach (SButton button in down) + activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true); + + // handle released keys + foreach (KeyValuePair prev in activeButtons) + { + if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) + activeButtons[prev.Key] = InputStatus.Released; + } + + return activeButtons; + } + + /// Get the status of a button relative to its previous status. + /// The previous button status. + /// Whether the button is currently down. + private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown) + { + if (isDown && oldStatus.IsDown()) + return InputStatus.Held; + if (isDown) + return InputStatus.Pressed; + return InputStatus.Released; + } + + /// Get the status of a button. + /// The current button states to check. + /// The button to check. + private InputStatus GetStatus(IDictionary activeButtons, SButton button) + { + return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; + } + + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) + { + // main buttons + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + + // directional pad + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } + } + } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4f5bd96b..182b90fc 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -53,6 +53,9 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; + /// Manages input visible to the game. + private SInputState Input => (SInputState)Game1.input; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -78,9 +81,6 @@ namespace StardewModdingAPI.Framework /**** ** Game state ****/ - /// The player input as of the previous tick. - private InputState PreviousInput = new InputState(); - /// The underlying watchers for convenience. These are accessible individually as separate properties. private readonly List Watchers = new List(); @@ -120,9 +120,6 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. private readonly Reflector Reflection; - /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. - private readonly HashSet SuppressButtons = new HashSet(); - /********* ** Accessors @@ -157,7 +154,7 @@ namespace StardewModdingAPI.Framework this.OnGameExiting = onGameExiting; if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection); - Game1.hooks = new SModHooks(this.UpdateControlInput); + Game1.input = new SInputState(); // init watchers Game1.locations = new ObservableCollection(); @@ -289,7 +286,7 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) this.OnGameInitialised(); - + /********* ** Update context *********/ @@ -390,16 +387,9 @@ namespace StardewModdingAPI.Framework *********/ if (Game1.game1.IsActive) { - // get input state - InputState inputState; - try - { - inputState = InputState.GetState(this.PreviousInput); - } - catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true - { - inputState = this.PreviousInput; - } + SInputState previousInputState = this.Input.Clone(); + SInputState inputState = this.Input; + inputState.TrueUpdate(); // raise events bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); @@ -425,7 +415,7 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -436,14 +426,14 @@ namespace StardewModdingAPI.Framework 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.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + this.Events.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)); } } else if (status == InputStatus.Released) { - this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons)); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), inputState.SuppressButtons)); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -454,7 +444,7 @@ namespace StardewModdingAPI.Framework 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.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); + this.Events.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)); } @@ -462,14 +452,11 @@ namespace StardewModdingAPI.Framework } // raise legacy state-changed events - if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); - if (inputState.MouseState != this.PreviousInput.MouseState) - this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); + if (inputState.RealKeyboard != previousInputState.RealKeyboard) + this.Events.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)); } - - // track state - this.PreviousInput = inputState; } /********* @@ -624,17 +611,6 @@ namespace StardewModdingAPI.Framework } } - /// Read the current input state for handling. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic) - { - this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState); - defaultLogic(); - } - /// The method called to draw everything to the screen. /// A snapshot of the game timing state. protected override void Draw(GameTime gameTime) @@ -1256,63 +1232,6 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ - /// Apply input suppression for the given input states. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) - { - // stop suppressing buttons once released - if (this.SuppressButtons.Count != 0) - { - InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState); - this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p)); - } - if (this.SuppressButtons.Count == 0) - return; - - // gather info - HashSet keyboardButtons = new HashSet(); - HashSet controllerButtons = new HashSet(); - HashSet mouseButtons = new HashSet(); - foreach (SButton button in this.SuppressButtons) - { - if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - mouseButtons.Add(button); - else if (button.TryGetKeyboard(out Keys key)) - keyboardButtons.Add(key); - else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) - controllerButtons.Add(button); - } - - // suppress keyboard keys - if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) - keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); - - // suppress controller keys - if (gamePadState.IsConnected && controllerButtons.Any()) - { - GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); - builder.SuppressButtons(controllerButtons); - gamePadState = builder.ToGamePadState(); - } - - // suppress mouse buttons - if (mouseButtons.Any()) - { - mouseState = new MouseState( - x: mouseState.X, - y: mouseState.Y, - scrollWheel: mouseState.ScrollWheelValue, - leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, - middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, - rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, - xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, - xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 - ); - } - } - /// Perform any cleanup needed when the player unloads a save and returns to the title screen. private void CleanupAfterReturnToTitle() { diff --git a/src/SMAPI/SModHooks.cs b/src/SMAPI/SModHooks.cs deleted file mode 100644 index 1b88d606..00000000 --- a/src/SMAPI/SModHooks.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Microsoft.Xna.Framework.Input; -using StardewValley; - -namespace StardewModdingAPI -{ - /// Intercepts predefined Stardew Valley mod hooks. - internal class SModHooks : ModHooks - { - /********* - ** Delegates - *********/ - /// A delegate invoked by the hook. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public delegate void UpdateControlInputDelegate(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action); - - - /********* - ** Properties - *********/ - /// The callback for . - private readonly UpdateControlInputDelegate UpdateControlInputHandler; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The callback for . - public SModHooks(UpdateControlInputDelegate updateControlInputHandler) - { - this.UpdateControlInputHandler = updateControlInputHandler; - } - - /// A hook invoked before the game processes player input. - /// The game's keyboard state for the current tick. - /// The game's mouse state for the current tick. - /// The game's controller state for the current tick. - /// The game's default logic. - public override void OnGame1_UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action action) - { - this.UpdateControlInputHandler(ref keyboardState, ref mouseState, ref gamePadState, action); - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index d7719e27..560d7bf4 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -92,7 +92,7 @@ - + @@ -263,7 +263,6 @@ - -- cgit From 2fcc4d92c4c29951d1c669ffe42ebbb78e9a23d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Apr 2018 14:28:48 -0400 Subject: fix released-button detection (#453) --- src/SMAPI/Framework/Input/SInputState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/Input') diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 62defa9f..1b224737 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -225,7 +225,7 @@ namespace StardewModdingAPI.Framework.Input activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true); // handle released keys - foreach (KeyValuePair prev in activeButtons) + foreach (KeyValuePair prev in previousStatuses) { if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) activeButtons[prev.Key] = InputStatus.Released; -- cgit From fec6adf82d5eda99c6bf34a21b0e01adc2b10d22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Apr 2018 21:55:26 -0400 Subject: fix build error on Linux/Mac --- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework/Input') diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index 5eeb7ef6..33557385 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -133,7 +132,7 @@ namespace StardewModdingAPI.Framework.Input rightThumbStick: this.RightStickPos, leftTrigger: this.LeftTrigger, rightTrigger: this.RightTrigger, - buttons: this.GetPressedButtons().ToArray() + buttons: this.GetBitmask(this.GetPressedButtons()) // MonoDevelop requires one bitmask here; don't specify multiple values ); } @@ -149,5 +148,15 @@ namespace StardewModdingAPI.Framework.Input yield return button; } } + + /// Get a bitmask representing the given buttons. + /// The buttons to represent. + private Buttons GetBitmask(IEnumerable buttons) + { + Buttons flag = 0; + foreach (Buttons button in buttons) + flag |= button; + return flag; + } } } -- cgit From 30bf40ab2b03600b66091a6cbd61515cf0721844 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 02:27:43 -0400 Subject: fix input suppression not working in some cases --- docs/release-notes.md | 1 + src/SMAPI/Framework/Input/SInputState.cs | 89 +++++++++++++++++--------------- src/SMAPI/Framework/SGame.cs | 1 + 3 files changed, 49 insertions(+), 42 deletions(-) (limited to 'src/SMAPI/Framework/Input') diff --git a/docs/release-notes.md b/docs/release-notes.md index ece388c7..d1a78aaa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,6 +23,7 @@ * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. * Fixed assets not reloaded consistently when the player switches language. + * Fixed input suppression not working consistently for clicks. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 1b224737..5e8efa62 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -81,22 +81,16 @@ namespace StardewModdingAPI.Framework.Input 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); - // get suppressed states - GamePadState suppressedController = realController; - KeyboardState suppressedKeyboard = realKeyboard; - MouseState suppressedMouse = realMouse; - if (this.SuppressButtons.Count > 0) - this.UpdateSuppression(activeButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); - - // update + // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - this.SuppressedController = suppressedController; - this.SuppressedKeyboard = suppressedKeyboard; - this.SuppressedMouse = suppressedMouse; this.MousePosition = mousePosition; + + // update suppressed states + this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); + this.UpdateSuppression(); } catch (InvalidOperationException) { @@ -104,6 +98,20 @@ namespace StardewModdingAPI.Framework.Input } } + /// Apply input suppression to current input. + public void UpdateSuppression() + { + GamePadState suppressedController = this.RealController; + KeyboardState suppressedKeyboard = this.RealKeyboard; + MouseState suppressedMouse = this.RealMouse; + + this.SuppressGivenStates(this.ActiveButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + + this.SuppressedController = suppressedController; + this.SuppressedKeyboard = suppressedKeyboard; + this.SuppressedMouse = suppressedMouse; + } + /// Get the gamepad state visible to the game. [Obsolete("This method should only be called by the game itself.")] public override GamePadState GetGamePadState() @@ -145,71 +153,68 @@ namespace StardewModdingAPI.Framework.Input return buttons.Any(button => this.IsDown(button.ToSButton())); } - /// Apply input suppression for the given input states. + + /********* + ** Private methods + *********/ + /// Whether input should be suppressed in the current context. + private bool ShouldSuppressNow() + { + return Game1.chatBox != null && !Game1.chatBox.isActive(); + } + + /// Apply input suppression to the given input states. /// The current button states to check. /// The game's keyboard state for the current tick. /// The game's mouse state for the current tick. /// The game's controller state for the current tick. - public void UpdateSuppression(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + private void SuppressGivenStates(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) { - // stop suppressing buttons once released - if (this.SuppressButtons.Count != 0) - this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); if (this.SuppressButtons.Count == 0) return; // gather info - HashSet keyboardButtons = new HashSet(); - HashSet controllerButtons = new HashSet(); - HashSet mouseButtons = new HashSet(); + HashSet suppressKeys = new HashSet(); + HashSet suppressButtons = new HashSet(); + HashSet suppressMouse = new HashSet(); foreach (SButton button in this.SuppressButtons) { if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - mouseButtons.Add(button); + suppressMouse.Add(button); else if (button.TryGetKeyboard(out Keys key)) - keyboardButtons.Add(key); + suppressKeys.Add(key); else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) - controllerButtons.Add(button); + suppressButtons.Add(button); } // suppress keyboard keys - if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any()) - keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray()); + if (keyboardState.GetPressedKeys().Any() && suppressKeys.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(suppressKeys).ToArray()); // suppress controller keys - if (gamePadState.IsConnected && controllerButtons.Any()) + if (gamePadState.IsConnected && suppressButtons.Any()) { GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); - builder.SuppressButtons(controllerButtons); + builder.SuppressButtons(suppressButtons); gamePadState = builder.ToGamePadState(); } // suppress mouse buttons - if (mouseButtons.Any()) + if (suppressMouse.Any()) { mouseState = new MouseState( x: mouseState.X, y: mouseState.Y, scrollWheel: mouseState.ScrollWheelValue, - leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton, - middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton, - rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton, - xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1, - xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2 + leftButton: suppressMouse.Contains(SButton.MouseLeft) ? ButtonState.Released : mouseState.LeftButton, + middleButton: suppressMouse.Contains(SButton.MouseMiddle) ? ButtonState.Released : mouseState.MiddleButton, + rightButton: suppressMouse.Contains(SButton.MouseRight) ? ButtonState.Released : mouseState.RightButton, + xButton1: suppressMouse.Contains(SButton.MouseX1) ? ButtonState.Released : mouseState.XButton1, + xButton2: suppressMouse.Contains(SButton.MouseX2) ? ButtonState.Released : mouseState.XButton2 ); } } - - /********* - ** Private methods - *********/ - /// Whether input should be suppressed in the current context. - private bool ShouldSuppressNow() - { - return Game1.chatBox != null && !Game1.chatBox.isActive(); - } - /// Get the status of all pressed or released buttons relative to their previous status. /// The previous button statuses. /// The keyboard state. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c8c30834..63f7f073 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -613,6 +613,7 @@ namespace StardewModdingAPI.Framework /********* ** Game update *********/ + this.Input.UpdateSuppression(); try { base.Update(gameTime); -- cgit From a059da747a4ebf5cc4257b449a85031f7842c291 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 May 2018 19:57:00 -0400 Subject: fix input suppression not working on the title menu (#527) --- src/SMAPI/Framework/Input/SInputState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/Input') diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 5e8efa62..27e40ab4 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Framework.Input /// Whether input should be suppressed in the current context. private bool ShouldSuppressNow() { - return Game1.chatBox != null && !Game1.chatBox.isActive(); + return Game1.chatBox == null || !Game1.chatBox.isActive(); } /// Apply input suppression to the given input states. -- cgit From d41fe6ff88b569f991f219c5f348d3688fba956f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Jun 2018 16:00:16 -0400 Subject: add input API --- docs/release-notes.md | 1 + src/SMAPI/Framework/CursorPosition.cs | 7 +++- src/SMAPI/Framework/Input/SInputState.cs | 28 +++++++++++--- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 54 +++++++++++++++++++++++++++ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 8 +++- src/SMAPI/Framework/SGame.cs | 25 ++++--------- src/SMAPI/IInputHelper.cs | 21 +++++++++++ src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 + 9 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 src/SMAPI/Framework/ModHelpers/InputHelper.cs create mode 100644 src/SMAPI/IInputHelper.cs (limited to 'src/SMAPI/Framework/Input') diff --git a/docs/release-notes.md b/docs/release-notes.md index 750fa37f..8824c0fb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * 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; 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 *********/ + /// The raw pixel position, not adjusted for the game zoom. + public Vector2 RawPixels { get; } + /// The pixel position relative to the top-left corner of the visible screen. public Vector2 ScreenPixels { get; } @@ -22,11 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. + /// The raw pixel position, not adjusted for the game zoom. /// The pixel position relative to the top-left corner of the visible screen. /// The tile position relative to the top-left corner of the map. /// The tile position that the game considers under the cursor for purposes of clicking actions. - 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/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 { - /// A summary of input changes during an update frame. + /// Manages the game's input state. internal sealed class SInputState : InputState { /********* @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.Input /// The maximum amount of direction to ignore for the left thumbstick. private const float LeftThumbstickDeadZone = 0.2f; + /// The cursor position on the screen adjusted for the zoom level. + private CursorPosition CursorPositionImpl; + /********* ** Accessors @@ -39,8 +42,8 @@ namespace StardewModdingAPI.Framework.Input /// A derivative of which suppresses the buttons in . public MouseState SuppressedMouse { get; private set; } - /// The mouse position on the screen adjusted for the zoom level. - public Point MousePosition { get; private set; } + /// The cursor position on the screen adjusted for the zoom level. + public ICursorPosition CursorPosition => this.CursorPositionImpl; /// The buttons which were pressed, held, or released. public IDictionary ActiveButtons { get; private set; } = new Dictionary(); @@ -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 *********/ + /// Get the current cursor position. + /// The raw pixel position from the mouse state. + 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); + } + /// Whether input should be suppressed in the current context. 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 +{ + /// Provides an API for checking and changing input state. + internal class InputHelper : BaseHelper, IInputHelper + { + /********* + ** Accessors + *********/ + /// Manages the game's input state. + private readonly SInputState InputState; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// Manages the game's input state. + public InputHelper(string modID, SInputState inputState) + : base(modID) + { + this.InputState = inputState; + } + + /// Get the current cursor position. + public ICursorPosition GetCursorPosition() + { + return this.InputState.CursorPosition; + } + + /// Get whether a button is currently pressed. + /// The button. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + 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 92cb9d94..1e07dafa 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -3,6 +3,7 @@ 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.Framework.Utilities; @@ -40,6 +41,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for loading content assets. public IContentHelper Content { get; } + /// An API for checking and changing input state. + public IInputHelper Input { get; } + /// An API for accessing private game code. public IReflectionHelper Reflection { get; } @@ -63,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod's unique ID. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. + /// Manages the game's input state. /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. @@ -75,7 +80,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func 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 contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -88,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)); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 18529728..560b54a4 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -54,9 +54,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager Events; - /// Manages input visible to the game. - private SInputState Input => (SInputState)Game1.input; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -101,7 +98,7 @@ namespace StardewModdingAPI.Framework private readonly IValueWatcher ActiveMenuWatcher; /// Tracks changes to the cursor position. - private readonly IValueWatcher CursorWatcher; + private readonly IValueWatcher CursorWatcher; /// Tracks changes to the mouse wheel scroll. private readonly IValueWatcher MouseWheelScrollWatcher; @@ -137,6 +134,9 @@ namespace StardewModdingAPI.Framework /// SMAPI's content manager. public ContentCoordinator ContentCore { get; private set; } + /// Manages input visible to the game. + public SInputState Input => (SInputState)Game1.input; + /// The game's core multiplayer utility. public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; @@ -174,7 +174,7 @@ namespace StardewModdingAPI.Framework // init watchers Game1.locations = new ObservableCollection(); - this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.MousePosition); + 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)); @@ -452,18 +452,7 @@ 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 = this.PreviousCursorPosition; - if (this.CursorWatcher.IsChanged) - { - // cursor position - Vector2 screenPixels = new Vector2(this.CursorWatcher.CurrentValue.X, this.CursorWatcher.CurrentValue.Y); - 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); - } + ICursorPosition cursor = this.Input.CursorPosition; // raise cursor moved event if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null) @@ -533,7 +522,7 @@ namespace StardewModdingAPI.Framework if (inputState.RealKeyboard != previousInputState.RealKeyboard) this.Events.Legacy_Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); if (inputState.RealMouse != previousInputState.RealMouse) - this.Events.Legacy_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))); } } 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 +{ + /// Provides an API for checking and changing input state. + public interface IInputHelper : IModLinked + { + /// Get the current cursor position. + ICursorPosition GetCursorPosition(); + + /// Get whether a button is currently pressed. + /// The button. + bool IsDown(SButton button); + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + bool IsSuppressed(SButton button); + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + void Suppress(SButton button); + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 48ad922b..76c12351 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -855,7 +855,7 @@ namespace StardewModdingAPI return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, events, 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 diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index b81f1359..f4aee551 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -114,6 +114,8 @@ + + -- cgit From 08e9c7e7d36cbf0720c93b395e688aeb5c86b1dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jun 2018 22:59:38 -0400 Subject: add absolute pixels to ICursorPosition, fix tile not updated if screen-relative pos didn't change (#546) --- docs/release-notes.md | 1 + src/SMAPI/Framework/CursorPosition.cs | 12 ++++++------ src/SMAPI/Framework/Input/SInputState.cs | 16 +++++++++------- src/SMAPI/ICursorPosition.cs | 3 +++ 4 files changed, 19 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework/Input') diff --git a/docs/release-notes.md b/docs/release-notes.md index b3ab2481..fa6501a3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -42,6 +42,7 @@ * Added support for custom seasonal tilesheets when loading an unpacked `.tbin` map. * Added Harmony DLL for internal use by SMAPI. (Mods should still include their own copy for backwards compatibility, and in case it's removed later. SMAPI will always load its own version though.) * Added option to suppress update checks for a specific mod in `StardewModdingAPI.config.json`. + * Added absolute pixels to `ICursorPosition`. * Update checks now use the update key order when deciding which to link to. * Fixed error if a mod loads a PNG while the game is loading (e.g. custom map tilesheets via `IAssetLoader`). * Fixed assets loaded by temporary content managers not being editable by mods. diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index aaf089d3..079917f2 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -8,8 +8,8 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// The raw pixel position, not adjusted for the game zoom. - public Vector2 RawPixels { get; } + /// The pixel position relative to the top-left corner of the in-game map. + public Vector2 AbsolutePixels { get; } /// The pixel position relative to the top-left corner of the visible screen. public Vector2 ScreenPixels { get; } @@ -25,13 +25,13 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// Construct an instance. - /// The raw pixel position, not adjusted for the game zoom. + /// The pixel position relative to the top-left corner of the in-game map. /// The pixel position relative to the top-left corner of the visible screen. /// The tile position relative to the top-left corner of the map. /// The tile position that the game considers under the cursor for purposes of clicking actions. - public CursorPosition(Vector2 rawPixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + public CursorPosition(Vector2 absolutePixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) { - this.RawPixels = rawPixels; + this.AbsolutePixels = absolutePixels; this.ScreenPixels = screenPixels; this.Tile = tile; this.GrabTile = grabTile; @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework /// An object to compare with this object. public bool Equals(ICursorPosition other) { - return other != null && this.ScreenPixels == other.ScreenPixels; + return other != null && this.AbsolutePixels == other.AbsolutePixels; } } } diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 44fd0618..0228db0d 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -82,15 +82,15 @@ namespace StardewModdingAPI.Framework.Input KeyboardState realKeyboard = Keyboard.GetState(); MouseState realMouse = Mouse.GetState(); var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); - Vector2 cursorRawPixelPos = new Vector2(this.RealMouse.X, this.RealMouse.Y); + Vector2 cursorAbsolutePos = new Vector2(realMouse.X + Game1.viewport.X, realMouse.Y + Game1.viewport.Y); // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; - if (this.CursorPositionImpl?.RawPixels != cursorRawPixelPos) - this.CursorPositionImpl = this.GetCursorPosition(cursorRawPixelPos); + if (this.CursorPositionImpl?.AbsolutePixels != cursorAbsolutePos) + this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos); // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); @@ -162,15 +162,17 @@ namespace StardewModdingAPI.Framework.Input ** Private methods *********/ /// Get the current cursor position. - /// The raw pixel position from the mouse state. - private CursorPosition GetCursorPosition(Vector2 rawPixelPos) + /// The current mouse state. + /// The absolute pixel position relative to the map. + private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels) { - 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 rawPixels = new Vector2(mouseState.X, mouseState.Y); + Vector2 screenPixels = rawPixels * new Vector2((float)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); + return new CursorPosition(absolutePixels, screenPixels, tile, grabTile); } /// Whether input should be suppressed in the current context. diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs index 78f4fc21..21c57db0 100644 --- a/src/SMAPI/ICursorPosition.cs +++ b/src/SMAPI/ICursorPosition.cs @@ -6,6 +6,9 @@ namespace StardewModdingAPI /// Represents a cursor position in the different coordinate systems. public interface ICursorPosition : IEquatable { + /// The pixel position relative to the top-left corner of the in-game map. + Vector2 AbsolutePixels { get; } + /// The pixel position relative to the top-left corner of the visible screen. Vector2 ScreenPixels { get; } -- cgit