summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs153
-rw-r--r--src/SMAPI/Framework/Input/InputState.cs78
-rw-r--r--src/SMAPI/Framework/SGame.cs78
3 files changed, 292 insertions, 17 deletions
diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
new file mode 100644
index 00000000..5eeb7ef6
--- /dev/null
+++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
@@ -0,0 +1,153 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace StardewModdingAPI.Framework.Input
+{
+ /// <summary>An abstraction for manipulating controller state.</summary>
+ internal class GamePadStateBuilder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The current button states.</summary>
+ private readonly IDictionary<SButton, ButtonState> ButtonStates;
+
+ /// <summary>The left trigger value.</summary>
+ private float LeftTrigger;
+
+ /// <summary>The right trigger value.</summary>
+ private float RightTrigger;
+
+ /// <summary>The left thumbstick position.</summary>
+ private Vector2 LeftStickPos;
+
+ /// <summary>The left thumbstick position.</summary>
+ private Vector2 RightStickPos;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="state">The initial controller state.</param>
+ public GamePadStateBuilder(GamePadState state)
+ {
+ this.ButtonStates = new Dictionary<SButton, ButtonState>
+ {
+ [SButton.DPadUp] = state.DPad.Up,
+ [SButton.DPadDown] = state.DPad.Down,
+ [SButton.DPadLeft] = state.DPad.Left,
+ [SButton.DPadRight] = state.DPad.Right,
+
+ [SButton.ControllerA] = state.Buttons.A,
+ [SButton.ControllerB] = state.Buttons.B,
+ [SButton.ControllerX] = state.Buttons.X,
+ [SButton.ControllerY] = state.Buttons.Y,
+ [SButton.LeftStick] = state.Buttons.LeftStick,
+ [SButton.RightStick] = state.Buttons.RightStick,
+ [SButton.LeftShoulder] = state.Buttons.LeftShoulder,
+ [SButton.RightShoulder] = state.Buttons.RightShoulder,
+ [SButton.ControllerBack] = state.Buttons.Back,
+ [SButton.ControllerStart] = state.Buttons.Start,
+ [SButton.BigButton] = state.Buttons.BigButton
+ };
+ this.LeftTrigger = state.Triggers.Left;
+ this.RightTrigger = state.Triggers.Right;
+ this.LeftStickPos = state.ThumbSticks.Left;
+ this.RightStickPos = state.ThumbSticks.Right;
+ }
+
+ /// <summary>Mark all matching buttons unpressed.</summary>
+ /// <param name="buttons">The buttons.</param>
+ public void SuppressButtons(IEnumerable<SButton> buttons)
+ {
+ foreach (SButton button in buttons)
+ this.SuppressButton(button);
+ }
+
+ /// <summary>Mark a button unpressed.</summary>
+ /// <param name="button">The button.</param>
+ public void SuppressButton(SButton button)
+ {
+ switch (button)
+ {
+ // left thumbstick
+ case SButton.LeftThumbstickUp:
+ if (this.LeftStickPos.Y > 0)
+ this.LeftStickPos.Y = 0;
+ break;
+ case SButton.LeftThumbstickDown:
+ if (this.LeftStickPos.Y < 0)
+ this.LeftStickPos.Y = 0;
+ break;
+ case SButton.LeftThumbstickLeft:
+ if (this.LeftStickPos.X < 0)
+ this.LeftStickPos.X = 0;
+ break;
+ case SButton.LeftThumbstickRight:
+ if (this.LeftStickPos.X > 0)
+ this.LeftStickPos.X = 0;
+ break;
+
+ // right thumbstick
+ case SButton.RightThumbstickUp:
+ if (this.RightStickPos.Y > 0)
+ this.RightStickPos.Y = 0;
+ break;
+ case SButton.RightThumbstickDown:
+ if (this.RightStickPos.Y < 0)
+ this.RightStickPos.Y = 0;
+ break;
+ case SButton.RightThumbstickLeft:
+ if (this.RightStickPos.X < 0)
+ this.RightStickPos.X = 0;
+ break;
+ case SButton.RightThumbstickRight:
+ if (this.RightStickPos.X > 0)
+ this.RightStickPos.X = 0;
+ break;
+
+ // triggers
+ case SButton.LeftTrigger:
+ this.LeftTrigger = 0;
+ break;
+ case SButton.RightTrigger:
+ this.RightTrigger = 0;
+ break;
+
+ // buttons
+ default:
+ if (this.ButtonStates.ContainsKey(button))
+ this.ButtonStates[button] = ButtonState.Released;
+ break;
+ }
+ }
+
+ /// <summary>Construct an equivalent gamepad state.</summary>
+ public GamePadState ToGamePadState()
+ {
+ return new GamePadState(
+ leftThumbStick: this.LeftStickPos,
+ rightThumbStick: this.RightStickPos,
+ leftTrigger: this.LeftTrigger,
+ rightTrigger: this.RightTrigger,
+ buttons: this.GetPressedButtons().ToArray()
+ );
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get all pressed buttons.</summary>
+ private IEnumerable<Buttons> GetPressedButtons()
+ {
+ foreach (var pair in this.ButtonStates)
+ {
+ if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button))
+ yield return button;
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs
index 7c8676e9..62337a6c 100644
--- a/src/SMAPI/Framework/Input/InputState.cs
+++ b/src/SMAPI/Framework/Input/InputState.cs
@@ -12,6 +12,16 @@ namespace StardewModdingAPI.Framework.Input
/*********
** Accessors
*********/
+ /// <summary>The maximum amount of direction to ignore for the left thumbstick.</summary>
+ private const float LeftThumbstickDeadZone = 0.2f;
+
+ /// <summary>The maximum amount of direction to ignore for the right thumbstick.</summary>
+ private const float RightThumbstickDeadZone = 0f;
+
+
+ /*********
+ ** Accessors
+ *********/
/// <summary>The underlying controller state.</summary>
public GamePadState ControllerState { get; }
@@ -48,7 +58,7 @@ namespace StardewModdingAPI.Framework.Input
this.MousePosition = new Point((int)(mouseState.X * (1.0 / Game1.options.zoomLevel)), (int)(mouseState.Y * (1.0 / Game1.options.zoomLevel))); // derived from Game1::getMouseX
// get button states
- SButton[] down = InputState.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray();
+ SButton[] down = this.GetPressedButtons(keyboardState, mouseState, controllerState).ToArray();
foreach (SButton button in down)
this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true);
foreach (KeyValuePair<SButton, InputStatus> prev in previousState.ActiveButtons)
@@ -109,7 +119,8 @@ namespace StardewModdingAPI.Framework.Input
/// <param name="keyboard">The keyboard state.</param>
/// <param name="mouse">The mouse state.</param>
/// <param name="controller">The controller state.</param>
- private static IEnumerable<SButton> GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller)
+ /// <remarks>Thumbstick direction logic derived from <see cref="ButtonCollection"/>.</remarks>
+ private IEnumerable<SButton> GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller)
{
// keyboard
foreach (Keys key in keyboard.GetPressedKeys())
@@ -130,28 +141,23 @@ namespace StardewModdingAPI.Framework.Input
// 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.Back == ButtonState.Pressed)
- yield return SButton.ControllerBack;
- if (controller.Buttons.BigButton == ButtonState.Pressed)
- yield return SButton.BigButton;
- if (controller.Buttons.LeftShoulder == ButtonState.Pressed)
- yield return SButton.LeftShoulder;
+ 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.RightShoulder == ButtonState.Pressed)
- yield return SButton.RightShoulder;
if (controller.Buttons.RightStick == ButtonState.Pressed)
yield return SButton.RightStick;
if (controller.Buttons.Start == ButtonState.Pressed)
yield return SButton.ControllerStart;
- if (controller.Buttons.X == ButtonState.Pressed)
- yield return SButton.ControllerX;
- if (controller.Buttons.Y == ButtonState.Pressed)
- yield return SButton.ControllerY;
+
+ // directional pad
if (controller.DPad.Up == ButtonState.Pressed)
yield return SButton.DPadUp;
if (controller.DPad.Down == ButtonState.Pressed)
@@ -160,11 +166,55 @@ namespace StardewModdingAPI.Framework.Input
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 > InputState.LeftThumbstickDeadZone)
+ yield return SButton.LeftThumbstickUp;
+ if (controller.ThumbSticks.Left.Y < -InputState.LeftThumbstickDeadZone)
+ yield return SButton.LeftThumbstickDown;
+ if (controller.ThumbSticks.Left.X > InputState.LeftThumbstickDeadZone)
+ yield return SButton.LeftThumbstickRight;
+ if (controller.ThumbSticks.Left.X < -InputState.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;
+ }
}
}
+
+ /// <summary>Get whether the right thumbstick should be considered outside the dead zone.</summary>
+ /// <param name="direction">The right thumbstick value.</param>
+ private bool IsRightThumbstickOutsideDeadZone(Vector2 direction)
+ {
+ return direction.Length() > 0.9f;
+ }
}
}
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 35e027d8..3b9a159f 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -120,6 +120,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection;
+ /// <summary>The buttons to suppress when the game next handles input. Each button is suppressed until it's released.</summary>
+ private readonly HashSet<SButton> SuppressButtons = new HashSet<SButton>();
+
/*********
** Accessors
@@ -154,6 +157,7 @@ namespace StardewModdingAPI.Framework
this.OnGameExiting = onGameExiting;
if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case
this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, reflection);
+ Game1.hooks = new SModHooks(this.UpdateControlInput);
// init watchers
Game1.locations = new ObservableCollection<GameLocation>();
@@ -420,7 +424,7 @@ namespace StardewModdingAPI.Framework
if (status == InputStatus.Pressed)
{
- this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton()));
+ this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons));
// legacy events
if (button.TryGetKeyboard(out Keys key))
@@ -438,7 +442,7 @@ namespace StardewModdingAPI.Framework
}
else if (status == InputStatus.Released)
{
- this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton()));
+ this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton(), this.SuppressButtons));
// legacy events
if (button.TryGetKeyboard(out Keys key))
@@ -539,7 +543,7 @@ namespace StardewModdingAPI.Framework
if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher<Vector2, SObject> _))
{
if (this.VerboseLogging)
- this.Monitor.Log($"Context: current location objects changed.", LogLevel.Trace);
+ this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace);
this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict));
}
@@ -619,6 +623,17 @@ namespace StardewModdingAPI.Framework
}
}
+ /// <summary>Read the current input state for handling.</summary>
+ /// <param name="keyboardState">The game's keyboard state for the current tick.</param>
+ /// <param name="mouseState">The game's mouse state for the current tick.</param>
+ /// <param name="gamePadState">The game's controller state for the current tick.</param>
+ /// <param name="defaultLogic">The game's default logic.</param>
+ public void UpdateControlInput(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState, Action defaultLogic)
+ {
+ this.ApplySuppression(ref keyboardState, ref mouseState, ref gamePadState);
+ defaultLogic();
+ }
+
/// <summary>The method called to draw everything to the screen.</summary>
/// <param name="gameTime">A snapshot of the game timing state.</param>
protected override void Draw(GameTime gameTime)
@@ -1240,6 +1255,63 @@ namespace StardewModdingAPI.Framework
/****
** Methods
****/
+ /// <summary>Apply input suppression for the given input states.</summary>
+ /// <param name="keyboardState">The game's keyboard state for the current tick.</param>
+ /// <param name="mouseState">The game's mouse state for the current tick.</param>
+ /// <param name="gamePadState">The game's controller state for the current tick.</param>
+ private void ApplySuppression(ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
+ {
+ // stop suppressing buttons once released
+ if (this.SuppressButtons.Count != 0)
+ {
+ InputState inputState = new InputState(this.PreviousInput, gamePadState, keyboardState, mouseState);
+ this.SuppressButtons.RemoveWhere(p => !inputState.IsDown(p));
+ }
+ if (this.SuppressButtons.Count == 0)
+ return;
+
+ // gather info
+ HashSet<Keys> keyboardButtons = new HashSet<Keys>();
+ HashSet<SButton> controllerButtons = new HashSet<SButton>();
+ HashSet<SButton> mouseButtons = new HashSet<SButton>();
+ 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
+ );
+ }
+ }
+
/// <summary>Perform any cleanup needed when the player unloads a save and returns to the title screen.</summary>
private void CleanupAfterReturnToTitle()
{