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());
}
}
}