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;
/// The buttons to press until the game next handles input.
private readonly HashSet CustomPressedKeys = new HashSet();
/// The buttons to consider released until the actual button is released.
private readonly HashSet CustomReleasedKeys = new HashSet();
/// The buttons which were actually down as of the last update, ignoring overrides.
private HashSet LastRealButtonPresses = new HashSet();
/*********
** Accessors
*********/
/// The controller state as of the last update.
public GamePadState LastController { get; private set; }
/// The keyboard state as of the last update.
public KeyboardState LastKeyboard { get; private set; }
/// The mouse state as of the last update.
public MouseState LastMouse { get; private set; }
/// The buttons which were pressed, held, or released as of the last update.
public IDictionary LastButtonStates { get; private set; } = new Dictionary();
/// The cursor position on the screen adjusted for the zoom level.
public ICursorPosition CursorPosition => this.CursorPositionImpl;
/*********
** Public methods
*********/
/// Get a copy of the current state.
public SInputState Clone()
{
return new SInputState
{
LastButtonStates = this.LastButtonStates,
LastController = this.LastController,
LastKeyboard = this.LastKeyboard,
LastMouse = this.LastMouse,
CursorPositionImpl = this.CursorPositionImpl
};
}
/// 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)
{
if (setDown)
{
this.CustomPressedKeys.Add(button);
this.CustomReleasedKeys.Remove(button);
}
else
{
this.CustomPressedKeys.Remove(button);
this.CustomReleasedKeys.Add(button);
}
}
/// 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);
}
/// 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()
{
try
{
float zoomMultiplier = (1f / Game1.options.zoomLevel);
// get real values
GamePadState controller = GamePad.GetState(PlayerIndex.One);
KeyboardState keyboard = Keyboard.GetState();
MouseState mouse = Mouse.GetState();
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 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, ref keyboard, ref mouse, ref controller))
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.LastButtonStates, pressedButtons, keyboard, mouse, controller);
// update
this.LastController = controller;
this.LastKeyboard = keyboard;
this.LastMouse = mouse;
this.LastButtonStates = activeButtons;
this.LastRealButtonPresses = reallyDown;
if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile)
{
this.LastPlayerTile = playerTilePos;
this.CursorPositionImpl = this.GetCursorPosition(mouse, cursorAbsolutePos, zoomMultiplier);
}
}
catch (InvalidOperationException)
{
// GetState() may crash for some players if window doesn't have focus but game1.IsActive == true
}
}
/// Apply input overrides to the current state.
public void ApplyOverrides()
{
GamePadState newController = this.LastController;
KeyboardState newKeyboard = this.LastKeyboard;
MouseState newMouse = this.LastMouse;
this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, ref newKeyboard, ref newMouse, ref newController);
this.LastController = newController;
this.LastKeyboard = newKeyboard;
this.LastMouse = newMouse;
}
/// 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 new GamePadState();
return this.LastController;
}
/// 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.LastKeyboard;
}
/// 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.LastMouse;
}
/// Get whether a given button was pressed or held.
/// The button to check.
public bool IsDown(SButton button)
{
return this.GetState(this.LastButtonStates, 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.LastButtonStates, 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);
}
/// Apply input overrides to the given states.
/// The buttons to mark pressed.
/// The buttons to mark released.
/// 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.
/// Returns whether any overrides were applied.
private bool ApplyOverrides(ISet pressed, ISet released, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
{
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 == 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 (gamePadState.IsConnected && button.TryGetController(out Buttons _))
controllerOverrides[button] = newState;
}
// override states
if (keyboardOverrides.Any())
keyboardState = new KeyboardStateBuilder(keyboardState).OverrideButtons(keyboardOverrides).ToState();
if (gamePadState.IsConnected && controllerOverrides.Any())
gamePadState = new GamePadStateBuilder(gamePadState).OverrideButtons(controllerOverrides).ToState();
if (mouseOverrides.Any())
mouseState = new MouseStateBuilder(mouseState).OverrideButtons(mouseOverrides).ToMouseState();
return true;
}
/// Get the state of all pressed or released buttons relative to their previous state.
/// The previous button states.
/// The currently pressed buttons.
/// The keyboard state.
/// The mouse state.
/// The controller state.
private IDictionary DeriveStates(IDictionary previousStates, HashSet pressedButtons, KeyboardState keyboard, MouseState mouse, GamePadState controller)
{
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(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;
}
}
}