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 { /// Manages the game's input state. internal sealed class SInputState : InputState { /********* ** Accessors *********/ /// 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; /// The player's last known tile position. private Vector2? LastPlayerTile; /********* ** 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 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(); /// 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, CursorPositionImpl = this.CursorPositionImpl }; } /// 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 { float zoomMultiplier = (1f / Game1.options.zoomLevel); // get new states GamePadState realController = GamePad.GetState(PlayerIndex.One); KeyboardState realKeyboard = Keyboard.GetState(); MouseState realMouse = Mouse.GetState(); var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); Vector2 cursorAbsolutePos = new Vector2((realMouse.X * zoomMultiplier) + Game1.viewport.X, (realMouse.Y * zoomMultiplier) + Game1.viewport.Y); Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; // update real states this.ActiveButtons = activeButtons; this.RealController = realController; this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) { this.LastPlayerTile = playerTilePos; this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos, zoomMultiplier); } // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); this.UpdateSuppression(); } catch (InvalidOperationException) { // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true } } /// 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() { if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff) return base.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())); } /// Get the status of a button. /// The button to check. public InputStatus GetStatus(SButton button) { return this.GetStatus(this.ActiveButtons, button); } /********* ** Private methods *********/ /// Get the current cursor position. /// The current mouse state. /// The absolute pixel position relative to the map, adjusted for pixel zoom. /// The multiplier applied to pixel coordinates to adjust them for pixel zoom. private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels, float zoomMultiplier) { Vector2 screenPixels = new Vector2(mouseState.X * zoomMultiplier, mouseState.Y * zoomMultiplier); 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(absolutePixels, screenPixels, tile, grabTile); } /// 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. private void SuppressGivenStates(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) { if (this.SuppressButtons.Count == 0) return; // gather info 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) suppressMouse.Add(button); else if (button.TryGetKeyboard(out Keys key)) suppressKeys.Add(key); else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) suppressButtons.Add(button); } // suppress keyboard keys if (keyboardState.GetPressedKeys().Any() && suppressKeys.Any()) keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(suppressKeys).ToArray()); // suppress controller keys if (gamePadState.IsConnected && suppressButtons.Any()) { GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); builder.SuppressButtons(suppressButtons); gamePadState = builder.ToGamePadState(); } // suppress mouse buttons if (suppressMouse.Any()) { mouseState = new MouseState( x: mouseState.X, y: mouseState.Y, scrollWheel: mouseState.ScrollWheelValue, 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 ); } } /// 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; } } }