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 previousStatuses) { 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; } } }