diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-04-25 02:47:07 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-04-25 02:47:07 -0400 |
commit | 3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c (patch) | |
tree | 5e77bae541dfd840dcfab478fbb9991651f8b51a /src/SMAPI/Framework/Input/SInputState.cs | |
parent | f1b7585e50b1a3a732787eec2fa9b6d7bd04a554 (diff) | |
download | SMAPI-3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c.tar.gz SMAPI-3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c.tar.bz2 SMAPI-3fcf58fcb5abcaf56dd7385fa7ef504ec9e90c5c.zip |
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.
Diffstat (limited to 'src/SMAPI/Framework/Input/SInputState.cs')
-rw-r--r-- | src/SMAPI/Framework/Input/SInputState.cs | 359 |
1 files changed, 359 insertions, 0 deletions
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 +{ + /// <summary>A summary of input changes during an update frame.</summary> + internal sealed class SInputState : InputState + { + /********* + ** Accessors + *********/ + /// <summary>The maximum amount of direction to ignore for the left thumbstick.</summary> + private const float LeftThumbstickDeadZone = 0.2f; + + + /********* + ** Accessors + *********/ + /// <summary>The controller state as of the last update.</summary> + public GamePadState RealController { get; private set; } + + /// <summary>The keyboard state as of the last update.</summary> + public KeyboardState RealKeyboard { get; private set; } + + /// <summary>The mouse state as of the last update.</summary> + public MouseState RealMouse { get; private set; } + + /// <summary>A derivative of <see cref="RealController"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary> + public GamePadState SuppressedController { get; private set; } + + /// <summary>A derivative of <see cref="RealKeyboard"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary> + public KeyboardState SuppressedKeyboard { get; private set; } + + /// <summary>A derivative of <see cref="RealMouse"/> which suppresses the buttons in <see cref="SuppressButtons"/>.</summary> + public MouseState SuppressedMouse { get; private set; } + + /// <summary>The mouse position on the screen adjusted for the zoom level.</summary> + public Point MousePosition { get; private set; } + + /// <summary>The buttons which were pressed, held, or released.</summary> + public IDictionary<SButton, InputStatus> ActiveButtons { get; private set; } = new Dictionary<SButton, InputStatus>(); + + /// <summary>The buttons to suppress when the game next handles input. Each button is suppressed until it's released.</summary> + public HashSet<SButton> SuppressButtons { get; } = new HashSet<SButton>(); + + + /********* + ** Public methods + *********/ + /// <summary>Get a copy of the current state.</summary> + public SInputState Clone() + { + return new SInputState + { + ActiveButtons = this.ActiveButtons, + RealController = this.RealController, + RealKeyboard = this.RealKeyboard, + RealMouse = this.RealMouse, + MousePosition = this.MousePosition + }; + } + + /// <summary>This method is called by the game, and does nothing since SMAPI will already have updated by that point.</summary> + [Obsolete("This method should only be called by the game itself.")] + public override void Update() { } + + /// <summary>Update the current button statuses for the given tick.</summary> + 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 + } + } + + /// <summary>Get the gamepad state visible to the game.</summary> + [Obsolete("This method should only be called by the game itself.")] + public override GamePadState GetGamePadState() + { + return this.ShouldSuppressNow() + ? this.SuppressedController + : this.RealController; + } + + /// <summary>Get the keyboard state visible to the game.</summary> + [Obsolete("This method should only be called by the game itself.")] + public override KeyboardState GetKeyboardState() + { + return this.ShouldSuppressNow() + ? this.SuppressedKeyboard + : this.RealKeyboard; + } + + /// <summary>Get the keyboard state visible to the game.</summary> + [Obsolete("This method should only be called by the game itself.")] + public override MouseState GetMouseState() + { + return this.ShouldSuppressNow() + ? this.SuppressedMouse + : this.RealMouse; + } + + /// <summary>Get whether a given button was pressed or held.</summary> + /// <param name="button">The button to check.</param> + public bool IsDown(SButton button) + { + return this.GetStatus(this.ActiveButtons, button).IsDown(); + } + + /// <summary>Get whether any of the given buttons were pressed or held.</summary> + /// <param name="buttons">The buttons to check.</param> + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + + /// <summary>Apply input suppression for the given input states.</summary> + /// <param name="activeButtons">The current button states to check.</param> + /// <param name="keyboardState">The game's keyboard state for the current tick.</param> + /// <param name="mouseState">The game's mouse state for the current tick.</param> + /// <param name="gamePadState">The game's controller state for the current tick.</param> + public void UpdateSuppression(IDictionary<SButton, InputStatus> 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<Keys> keyboardButtons = new HashSet<Keys>(); + HashSet<SButton> controllerButtons = new HashSet<SButton>(); + HashSet<SButton> mouseButtons = new HashSet<SButton>(); + 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 + *********/ + /// <summary>Whether input should be suppressed in the current context.</summary> + private bool ShouldSuppressNow() + { + return Game1.chatBox != null && !Game1.chatBox.isActive(); + } + + /// <summary>Get the status of all pressed or released buttons relative to their previous status.</summary> + /// <param name="previousStatuses">The previous button statuses.</param> + /// <param name="keyboard">The keyboard state.</param> + /// <param name="mouse">The mouse state.</param> + /// <param name="controller">The controller state.</param> + private IDictionary<SButton, InputStatus> DeriveStatuses(IDictionary<SButton, InputStatus> previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + IDictionary<SButton, InputStatus> activeButtons = new Dictionary<SButton, InputStatus>(); + + // 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<SButton, InputStatus> prev in activeButtons) + { + if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) + activeButtons[prev.Key] = InputStatus.Released; + } + + return activeButtons; + } + + /// <summary>Get the status of a button relative to its previous status.</summary> + /// <param name="oldStatus">The previous button status.</param> + /// <param name="isDown">Whether the button is currently down.</param> + private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown) + { + if (isDown && oldStatus.IsDown()) + return InputStatus.Held; + if (isDown) + return InputStatus.Pressed; + return InputStatus.Released; + } + + /// <summary>Get the status of a button.</summary> + /// <param name="activeButtons">The current button states to check.</param> + /// <param name="button">The button to check.</param> + private InputStatus GetStatus(IDictionary<SButton, InputStatus> activeButtons, SButton button) + { + return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; + } + + /// <summary>Get the buttons pressed in the given stats.</summary> + /// <param name="keyboard">The keyboard state.</param> + /// <param name="mouse">The mouse state.</param> + /// <param name="controller">The controller state.</param> + /// <remarks>Thumbstick direction logic derived from <see cref="ButtonCollection"/>.</remarks> + private IEnumerable<SButton> 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; + } + } + } + + /// <summary>Get whether the right thumbstick should be considered outside the dead zone.</summary> + /// <param name="direction">The right thumbstick value.</param> + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } + } +} |