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 cursor position on the screen adjusted for the zoom level. private CursorPosition CursorPositionImpl = new(Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero); /// The player's last known tile position. private Vector2? LastPlayerTile; /// The buttons to press until the game next handles input. private readonly HashSet CustomPressedKeys = new(); /// The buttons to consider released until the actual button is released. private readonly HashSet CustomReleasedKeys = new(); /// Whether there are new overrides in or that haven't been applied to the previous state. private bool HasNewOverrides; /********* ** Accessors *********/ /// The controller state as of the last update, with overrides applied. public GamePadState ControllerState { get; private set; } /// The keyboard state as of the last update, with overrides applied. public KeyboardState KeyboardState { get; private set; } /// The mouse state as of the last update, with overrides applied. public MouseState MouseState { get; private set; } /// The buttons which were pressed, held, or released as of the last update. public IDictionary ButtonStates { get; private set; } = new Dictionary(); /// The cursor position on the screen adjusted for the zoom level. public ICursorPosition CursorPosition => this.CursorPositionImpl; /********* ** Public methods *********/ /// 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 states for the given tick. public void TrueUpdate() { // update base state base.Update(); // update SMAPI extended data // note: Stardew Valley is *not* in UI mode when this code runs try { float zoomMultiplier = (1f / Game1.options.zoomLevel); // get real values var controller = new GamePadStateBuilder(base.GetGamePadState()); var keyboard = new KeyboardStateBuilder(base.GetKeyboardState()); var mouse = new MouseStateBuilder(base.GetMouseState()); Vector2 cursorAbsolutePos = new((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : null; HashSet reallyDown = new HashSet(this.GetPressedButtons(keyboard, mouse, controller)); // apply overrides bool hasOverrides = false; if (this.CustomPressedKeys.Count > 0 || this.CustomReleasedKeys.Count > 0) { // reset overrides that no longer apply this.CustomPressedKeys.RemoveWhere(key => reallyDown.Contains(key)); this.CustomReleasedKeys.RemoveWhere(key => !reallyDown.Contains(key)); // apply overrides if (this.ApplyOverrides(this.CustomPressedKeys, this.CustomReleasedKeys, controller, keyboard, mouse)) hasOverrides = true; // remove pressed keys this.CustomPressedKeys.Clear(); } // get button states var pressedButtons = hasOverrides ? new HashSet(this.GetPressedButtons(keyboard, mouse, controller)) : reallyDown; var activeButtons = this.DeriveStates(this.ButtonStates, pressedButtons); // update this.HasNewOverrides = false; this.ControllerState = controller.GetState(); this.KeyboardState = keyboard.GetState(); this.MouseState = mouse.GetState(); this.ButtonStates = activeButtons; if (cursorAbsolutePos != this.CursorPositionImpl.AbsolutePixels || playerTilePos != this.LastPlayerTile) { this.LastPlayerTile = playerTilePos; this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier); } } 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. public override GamePadState GetGamePadState() { return this.ControllerState; } /// Get the keyboard state visible to the game. public override KeyboardState GetKeyboardState() { return this.KeyboardState; } /// Get the keyboard state visible to the game. public override MouseState GetMouseState() { return this.MouseState; } /// Override the state for a button. /// The button to override. /// Whether to mark it pressed; else mark it released. public void OverrideButton(SButton button, bool setDown) { bool changed = setDown ? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button) : this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button); if (changed) this.HasNewOverrides = true; } /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. /// The button to check. public bool IsSuppressed(SButton button) { return this.CustomReleasedKeys.Contains(button); } /// Apply input overrides to the current state. public void ApplyOverrides() { if (this.HasNewOverrides) { var controller = new GamePadStateBuilder(this.ControllerState); var keyboard = new KeyboardStateBuilder(this.KeyboardState); var mouse = new MouseStateBuilder(this.MouseState); if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse)) { this.ControllerState = controller.GetState(); this.KeyboardState = keyboard.GetState(); this.MouseState = mouse.GetState(); } } } /// Get whether a given button was pressed or held. /// The button to check. public bool IsDown(SButton button) { return this.GetState(this.ButtonStates, 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 state of a button. /// The button to check. public SButtonState GetState(SButton button) { return this.GetState(this.ButtonStates, 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(mouseState.X * zoomMultiplier, mouseState.Y * zoomMultiplier); Vector2 tile = new((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); } /// Apply input overrides to the given states. /// The buttons to mark pressed. /// The buttons to mark released. /// The game's controller state for the current tick. /// The game's keyboard state for the current tick. /// The game's mouse state for the current tick. /// Returns whether any overrides were applied. private bool ApplyOverrides(ISet pressed, ISet released, GamePadStateBuilder controller, KeyboardStateBuilder keyboard, MouseStateBuilder mouse) { if (pressed.Count == 0 && released.Count == 0) return false; // group keys by type IDictionary keyboardOverrides = new Dictionary(); IDictionary controllerOverrides = new Dictionary(); IDictionary mouseOverrides = new Dictionary(); foreach (var button in pressed.Concat(released)) { var newState = this.DeriveState( oldState: this.GetState(button), isDown: pressed.Contains(button) ); if (button is SButton.MouseLeft or SButton.MouseMiddle or SButton.MouseRight or SButton.MouseX1 or SButton.MouseX2) mouseOverrides[button] = newState; else if (button.TryGetKeyboard(out Keys _)) keyboardOverrides[button] = newState; else if (controller.IsConnected && button.TryGetController(out Buttons _)) controllerOverrides[button] = newState; } // override states if (keyboardOverrides.Any()) keyboard.OverrideButtons(keyboardOverrides); if (controller.IsConnected && controllerOverrides.Any()) controller.OverrideButtons(controllerOverrides); if (mouseOverrides.Any()) mouse.OverrideButtons(mouseOverrides); return true; } /// Get the state of all pressed or released buttons relative to their previous state. /// The previous button states. /// The currently pressed buttons. private IDictionary DeriveStates(IDictionary previousStates, HashSet pressedButtons) { IDictionary activeButtons = new Dictionary(); // handle pressed keys foreach (SButton button in pressedButtons) activeButtons[button] = this.DeriveState(this.GetState(previousStates, button), isDown: true); // handle released keys foreach (KeyValuePair prev in previousStates) { if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) activeButtons[prev.Key] = SButtonState.Released; } return activeButtons; } /// Get the state of a button relative to its previous state. /// The previous button state. /// Whether the button is currently down. private SButtonState DeriveState(SButtonState oldState, bool isDown) { if (isDown && oldState.IsDown()) return SButtonState.Held; if (isDown) return SButtonState.Pressed; return SButtonState.Released; } /// Get the state of a button. /// The current button states to check. /// The button to check. private SButtonState GetState(IDictionary activeButtons, SButton button) { return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.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(KeyboardStateBuilder keyboard, MouseStateBuilder mouse, GamePadStateBuilder controller) { return keyboard .GetPressedButtons() .Concat(mouse.GetPressedButtons()) .Concat(controller.GetPressedButtons()); } } }