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;
/// 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();
/// Whether there are new overrides in or that haven't been applied to the previous state.
private bool HasNewOverrides;
/// The game tick when the input state was last updated.
private uint? LastUpdateTick;
/*********
** 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
*********/
/// Update the current button states for the given tick. This does nothing if the input has already been updated for this tick (e.g. because SMAPI updated it before the game update).
public override void Update()
{
// skip if already updated
if (this.LastUpdateTick == SCore.TicksElapsed)
return;
this.LastUpdateTick = SCore.TicksElapsed;
// update base state
base.Update();
// update SMAPI extended data
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 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, 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 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 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 == 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;
}
/// 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());
}
}
}