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
{
/// A summary of input changes during an update frame.
internal sealed class SInputState : InputState
{
/*********
** Accessors
*********/
/// The maximum amount of direction to ignore for the left thumbstick.
private const float LeftThumbstickDeadZone = 0.2f;
/*********
** Accessors
*********/
/// The controller state as of the last update.
public GamePadState RealController { get; private set; }
/// The keyboard state as of the last update.
public KeyboardState RealKeyboard { get; private set; }
/// The mouse state as of the last update.
public MouseState RealMouse { get; private set; }
/// A derivative of which suppresses the buttons in .
public GamePadState SuppressedController { get; private set; }
/// A derivative of which suppresses the buttons in .
public KeyboardState SuppressedKeyboard { get; private set; }
/// A derivative of which suppresses the buttons in .
public MouseState SuppressedMouse { get; private set; }
/// The mouse position on the screen adjusted for the zoom level.
public Point MousePosition { get; private set; }
/// The buttons which were pressed, held, or released.
public IDictionary ActiveButtons { get; private set; } = new Dictionary();
/// The buttons to suppress when the game next handles input. Each button is suppressed until it's released.
public HashSet SuppressButtons { get; } = new HashSet();
/*********
** Public methods
*********/
/// Get a copy of the current state.
public SInputState Clone()
{
return new SInputState
{
ActiveButtons = this.ActiveButtons,
RealController = this.RealController,
RealKeyboard = this.RealKeyboard,
RealMouse = this.RealMouse,
MousePosition = this.MousePosition
};
}
/// 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 statuses for the given tick.
public void TrueUpdate()
{
try
{
// get new states
GamePadState realController = GamePad.GetState(PlayerIndex.One);
KeyboardState realKeyboard = Keyboard.GetState();
MouseState realMouse = Mouse.GetState();
Point mousePosition = new Point((int)(this.RealMouse.X * (1.0 / Game1.options.zoomLevel)), (int)(this.RealMouse.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX
var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
// get suppressed states
GamePadState suppressedController = realController;
KeyboardState suppressedKeyboard = realKeyboard;
MouseState suppressedMouse = realMouse;
if (this.SuppressButtons.Count > 0)
this.UpdateSuppression(activeButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController);
// update
this.ActiveButtons = activeButtons;
this.RealController = realController;
this.RealKeyboard = realKeyboard;
this.RealMouse = realMouse;
this.SuppressedController = suppressedController;
this.SuppressedKeyboard = suppressedKeyboard;
this.SuppressedMouse = suppressedMouse;
this.MousePosition = mousePosition;
}
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.
[Obsolete("This method should only be called by the game itself.")]
public override GamePadState GetGamePadState()
{
return this.ShouldSuppressNow()
? this.SuppressedController
: this.RealController;
}
/// 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.ShouldSuppressNow()
? this.SuppressedKeyboard
: this.RealKeyboard;
}
/// 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.ShouldSuppressNow()
? this.SuppressedMouse
: this.RealMouse;
}
/// Get whether a given button was pressed or held.
/// The button to check.
public bool IsDown(SButton button)
{
return this.GetStatus(this.ActiveButtons, 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()));
}
/// Apply input suppression for the given input states.
/// The current button states to check.
/// 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.
public void UpdateSuppression(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
{
// stop suppressing buttons once released
if (this.SuppressButtons.Count != 0)
this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown());
if (this.SuppressButtons.Count == 0)
return;
// gather info
HashSet keyboardButtons = new HashSet();
HashSet controllerButtons = new HashSet();
HashSet mouseButtons = new HashSet();
foreach (SButton button in this.SuppressButtons)
{
if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2)
mouseButtons.Add(button);
else if (button.TryGetKeyboard(out Keys key))
keyboardButtons.Add(key);
else if (gamePadState.IsConnected && button.TryGetController(out Buttons _))
controllerButtons.Add(button);
}
// suppress keyboard keys
if (keyboardState.GetPressedKeys().Any() && keyboardButtons.Any())
keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(keyboardButtons).ToArray());
// suppress controller keys
if (gamePadState.IsConnected && controllerButtons.Any())
{
GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState);
builder.SuppressButtons(controllerButtons);
gamePadState = builder.ToGamePadState();
}
// suppress mouse buttons
if (mouseButtons.Any())
{
mouseState = new MouseState(
x: mouseState.X,
y: mouseState.Y,
scrollWheel: mouseState.ScrollWheelValue,
leftButton: mouseButtons.Contains(SButton.MouseLeft) ? ButtonState.Pressed : mouseState.LeftButton,
middleButton: mouseButtons.Contains(SButton.MouseMiddle) ? ButtonState.Pressed : mouseState.MiddleButton,
rightButton: mouseButtons.Contains(SButton.MouseRight) ? ButtonState.Pressed : mouseState.RightButton,
xButton1: mouseButtons.Contains(SButton.MouseX1) ? ButtonState.Pressed : mouseState.XButton1,
xButton2: mouseButtons.Contains(SButton.MouseX2) ? ButtonState.Pressed : mouseState.XButton2
);
}
}
/*********
** Private methods
*********/
/// Whether input should be suppressed in the current context.
private bool ShouldSuppressNow()
{
return Game1.chatBox != null && !Game1.chatBox.isActive();
}
/// Get the status of all pressed or released buttons relative to their previous status.
/// The previous button statuses.
/// The keyboard state.
/// The mouse state.
/// The controller state.
private IDictionary DeriveStatuses(IDictionary previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller)
{
IDictionary activeButtons = new Dictionary();
// handle pressed keys
SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray();
foreach (SButton button in down)
activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true);
// handle released keys
foreach (KeyValuePair prev in activeButtons)
{
if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key))
activeButtons[prev.Key] = InputStatus.Released;
}
return activeButtons;
}
/// Get the status of a button relative to its previous status.
/// The previous button status.
/// Whether the button is currently down.
private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown)
{
if (isDown && oldStatus.IsDown())
return InputStatus.Held;
if (isDown)
return InputStatus.Pressed;
return InputStatus.Released;
}
/// Get the status of a button.
/// The current button states to check.
/// The button to check.
private InputStatus GetStatus(IDictionary activeButtons, SButton button)
{
return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.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;
}
}
}