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>Manages the game's input state.</summary> internal sealed class SInputState : InputState { /********* ** Accessors *********/ /// <summary>The cursor position on the screen adjusted for the zoom level.</summary> private CursorPosition CursorPositionImpl; /// <summary>The player's last known tile position.</summary> private Vector2? LastPlayerTile; /// <summary>The buttons to press until the game next handles input.</summary> private readonly HashSet<SButton> CustomPressedKeys = new HashSet<SButton>(); /// <summary>The buttons to consider released until the actual button is released.</summary> private readonly HashSet<SButton> CustomReleasedKeys = new HashSet<SButton>(); /// <summary>Whether there are new overrides in <see cref="CustomPressedKeys"/> or <see cref="CustomReleasedKeys"/> that haven't been applied to the previous state.</summary> private bool HasNewOverrides; /********* ** Accessors *********/ /// <summary>The controller state as of the last update.</summary> public GamePadState LastController { get; private set; } /// <summary>The keyboard state as of the last update.</summary> public KeyboardState LastKeyboard { get; private set; } /// <summary>The mouse state as of the last update.</summary> public MouseState LastMouse { get; private set; } /// <summary>The buttons which were pressed, held, or released as of the last update.</summary> public IDictionary<SButton, SButtonState> LastButtonStates { get; private set; } = new Dictionary<SButton, SButtonState>(); /// <summary>The cursor position on the screen adjusted for the zoom level.</summary> public ICursorPosition CursorPosition => this.CursorPositionImpl; /********* ** Public methods *********/ /// <summary>Get a copy of the current state.</summary> public SInputState Clone() { return new SInputState { LastButtonStates = this.LastButtonStates, LastController = this.LastController, LastKeyboard = this.LastKeyboard, LastMouse = this.LastMouse, CursorPositionImpl = this.CursorPositionImpl }; } /// <summary>Override the state for a button.</summary> /// <param name="button">The button to override.</param> /// <param name="setDown">Whether to mark it pressed; else mark it released.</param> 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; } /// <summary>Get whether a mod has indicated the key was already handled, so the game shouldn't handle it.</summary> /// <param name="button">The button to check.</param> public bool IsSuppressed(SButton button) { return this.CustomReleasedKeys.Contains(button); } /// <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 states for the given tick.</summary> public void TrueUpdate() { try { float zoomMultiplier = (1f / Game1.options.zoomLevel); // get real values var controller = new GamePadStateBuilder(); var keyboard = new KeyboardStateBuilder(); var mouse = new MouseStateBuilder(); Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; HashSet<SButton> reallyDown = new HashSet<SButton>(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<SButton>(this.GetPressedButtons(keyboard, mouse, controller)) : reallyDown; var activeButtons = this.DeriveStates(this.LastButtonStates, pressedButtons); // update this.HasNewOverrides = false; this.LastController = controller.GetState(); this.LastKeyboard = keyboard.GetState(); this.LastMouse = mouse.GetState(); this.LastButtonStates = activeButtons; if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) { this.LastPlayerTile = playerTilePos; this.CursorPositionImpl = this.GetCursorPosition(this.LastMouse, cursorAbsolutePos, zoomMultiplier); } } catch (InvalidOperationException) { // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true } } /// <summary>Apply input overrides to the current state.</summary> public void ApplyOverrides() { if (this.HasNewOverrides) { var controller = new GamePadStateBuilder(this.LastController); var keyboard = new KeyboardStateBuilder(this.LastKeyboard); var mouse = new MouseStateBuilder(this.LastMouse); if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse)) { this.LastController = controller.GetState(); this.LastKeyboard = keyboard.GetState(); this.LastMouse = mouse.GetState(); } } } /// <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() { if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff) return new GamePadState(); return this.LastController; } /// <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.LastKeyboard; } /// <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.LastMouse; } /// <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.GetState(this.LastButtonStates, 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>Get the state of a button.</summary> /// <param name="button">The button to check.</param> public SButtonState GetState(SButton button) { return this.GetState(this.LastButtonStates, button); } /********* ** Private methods *********/ /// <summary>Get the current cursor position.</summary> /// <param name="mouseState">The current mouse state.</param> /// <param name="absolutePixels">The absolute pixel position relative to the map, adjusted for pixel zoom.</param> /// <param name="zoomMultiplier">The multiplier applied to pixel coordinates to adjust them for pixel zoom.</param> 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); } /// <summary>Apply input overrides to the given states.</summary> /// <param name="pressed">The buttons to mark pressed.</param> /// <param name="released">The buttons to mark released.</param> /// <param name="controller">The game's controller state for the current tick.</param> /// <param name="keyboard">The game's keyboard state for the current tick.</param> /// <param name="mouse">The game's mouse state for the current tick.</param> /// <returns>Returns whether any overrides were applied.</returns> private bool ApplyOverrides(ISet<SButton> pressed, ISet<SButton> released, GamePadStateBuilder controller, KeyboardStateBuilder keyboard, MouseStateBuilder mouse) { if (pressed.Count == 0 && released.Count == 0) return false; // group keys by type IDictionary<SButton, SButtonState> keyboardOverrides = new Dictionary<SButton, SButtonState>(); IDictionary<SButton, SButtonState> controllerOverrides = new Dictionary<SButton, SButtonState>(); IDictionary<SButton, SButtonState> mouseOverrides = new Dictionary<SButton, SButtonState>(); foreach (var button in pressed.Concat(released)) { var newState = this.DeriveState( oldState: this.GetState(button), isDown: pressed.Contains(button) ); if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == 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; } /// <summary>Get the state of all pressed or released buttons relative to their previous state.</summary> /// <param name="previousStates">The previous button states.</param> /// <param name="pressedButtons">The currently pressed buttons.</param> private IDictionary<SButton, SButtonState> DeriveStates(IDictionary<SButton, SButtonState> previousStates, HashSet<SButton> pressedButtons) { IDictionary<SButton, SButtonState> activeButtons = new Dictionary<SButton, SButtonState>(); // handle pressed keys foreach (SButton button in pressedButtons) activeButtons[button] = this.DeriveState(this.GetState(previousStates, button), isDown: true); // handle released keys foreach (KeyValuePair<SButton, SButtonState> prev in previousStates) { if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) activeButtons[prev.Key] = SButtonState.Released; } return activeButtons; } /// <summary>Get the state of a button relative to its previous state.</summary> /// <param name="oldState">The previous button state.</param> /// <param name="isDown">Whether the button is currently down.</param> private SButtonState DeriveState(SButtonState oldState, bool isDown) { if (isDown && oldState.IsDown()) return SButtonState.Held; if (isDown) return SButtonState.Pressed; return SButtonState.Released; } /// <summary>Get the state of a button.</summary> /// <param name="activeButtons">The current button states to check.</param> /// <param name="button">The button to check.</param> private SButtonState GetState(IDictionary<SButton, SButtonState> activeButtons, SButton button) { return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.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(KeyboardStateBuilder keyboard, MouseStateBuilder mouse, GamePadStateBuilder controller) { return keyboard .GetPressedButtons() .Concat(mouse.GetPressedButtons()) .Concat(controller.GetPressedButtons()); } } }