summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/Input/InputState.cs163
-rw-r--r--src/SMAPI/Framework/Input/InputStatus.cs29
-rw-r--r--src/SMAPI/Framework/SGame.cs193
-rw-r--r--src/SMAPI/Framework/Serialisation/ColorConverter.cs82
-rw-r--r--src/SMAPI/Framework/Serialisation/JsonHelper.cs8
-rw-r--r--src/SMAPI/Framework/Serialisation/PointConverter.cs79
-rw-r--r--src/SMAPI/Framework/Serialisation/RectangleConverter.cs84
-rw-r--r--src/SMAPI/SButton.cs15
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj5
9 files changed, 519 insertions, 139 deletions
diff --git a/src/SMAPI/Framework/Input/InputState.cs b/src/SMAPI/Framework/Input/InputState.cs
new file mode 100644
index 00000000..8b0108ae
--- /dev/null
+++ b/src/SMAPI/Framework/Input/InputState.cs
@@ -0,0 +1,163 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.Input
+{
+ /// <summary>A summary of input changes during an update frame.</summary>
+ internal class InputState
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The underlying controller state.</summary>
+ public GamePadState ControllerState { get; }
+
+ /// <summary>The underlying keyboard state.</summary>
+ public KeyboardState KeyboardState { get; }
+
+ /// <summary>The underlying mouse state.</summary>
+ public MouseState MouseState { get; }
+
+ /// <summary>The mouse position on the screen adjusted for the zoom level.</summary>
+ public Point MousePosition { get; }
+
+ /// <summary>The buttons which were pressed, held, or released.</summary>
+ public IDictionary<SButton, InputStatus> ActiveButtons { get; } = new Dictionary<SButton, InputStatus>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an empty instance.</summary>
+ public InputState() { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="previousState">The previous input state.</param>
+ /// <param name="controllerState">The current controller state.</param>
+ /// <param name="keyboardState">The current keyboard state.</param>
+ /// <param name="mouseState">The current mouse state.</param>
+ public InputState(InputState previousState, GamePadState controllerState, KeyboardState keyboardState, MouseState mouseState)
+ {
+ // init properties
+ this.ControllerState = controllerState;
+ this.KeyboardState = keyboardState;
+ this.MouseState = mouseState;
+ 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();
+ foreach (SButton button in down)
+ this.ActiveButtons[button] = this.GetStatus(previousState.GetStatus(button), isDown: true);
+ foreach (KeyValuePair<SButton, InputStatus> prev in previousState.ActiveButtons)
+ {
+ if (prev.Value.IsDown() && !this.ActiveButtons.ContainsKey(prev.Key))
+ this.ActiveButtons[prev.Key] = InputStatus.Released;
+ }
+ }
+
+ /// <summary>Get the status of a button.</summary>
+ /// <param name="button">The button to check.</param>
+ public InputStatus GetStatus(SButton button)
+ {
+ return this.ActiveButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None;
+ }
+
+ /// <summary>Get whether a given button was pressed or held.</summary>
+ /// <param name="button">The button to check.</param>
+ public bool IsDown(SButton button)
+ {
+ return this.GetStatus(button).IsDown();
+ }
+
+ /// <summary>Get the current input state.</summary>
+ /// <param name="previousState">The previous input state.</param>
+ public static InputState GetState(InputState previousState)
+ {
+ GamePadState controllerState = GamePad.GetState(PlayerIndex.One);
+ KeyboardState keyboardState = Keyboard.GetState();
+ MouseState mouseState = Mouse.GetState();
+
+ return new InputState(previousState, controllerState, keyboardState, mouseState);
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the status of a button.</summary>
+ /// <param name="oldStatus">The previous button status.</param>
+ /// <param name="isDown">Whether the button is currently down.</param>
+ public InputStatus GetStatus(InputStatus oldStatus, bool isDown)
+ {
+ if (isDown && oldStatus.IsDown())
+ return InputStatus.Held;
+ if (isDown)
+ return InputStatus.Pressed;
+ return InputStatus.Released;
+ }
+
+ /// <summary>Get the buttons pressed in the given stats.</summary>
+ /// <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)
+ {
+ // 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)
+ {
+ 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.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;
+ 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;
+ if (controller.Triggers.Left > 0.2f)
+ yield return SButton.LeftTrigger;
+ if (controller.Triggers.Right > 0.2f)
+ yield return SButton.RightTrigger;
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Input/InputStatus.cs b/src/SMAPI/Framework/Input/InputStatus.cs
new file mode 100644
index 00000000..99b0006c
--- /dev/null
+++ b/src/SMAPI/Framework/Input/InputStatus.cs
@@ -0,0 +1,29 @@
+namespace StardewModdingAPI.Framework.Input
+{
+ /// <summary>The input status for a button during an update frame.</summary>
+ internal enum InputStatus
+ {
+ /// <summary>The button was neither pressed, held, nor released.</summary>
+ None,
+
+ /// <summary>The button was pressed in this frame.</summary>
+ Pressed,
+
+ /// <summary>The button has been held since the last frame.</summary>
+ Held,
+
+ /// <summary>The button was released in this frame.</summary>
+ Released
+ }
+
+ /// <summary>Extension methods for <see cref="InputStatus"/>.</summary>
+ internal static class InputStatusExtensions
+ {
+ /// <summary>Whether the button was pressed or held.</summary>
+ /// <param name="status">The button status.</param>
+ public static bool IsDown(this InputStatus status)
+ {
+ return status == InputStatus.Held || status == InputStatus.Pressed;
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 0a614f17..2eb2da99 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -10,6 +10,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using StardewModdingAPI.Events;
+using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
@@ -53,20 +54,8 @@ namespace StardewModdingAPI.Framework
/****
** Game state
****/
- /// <summary>A record of the buttons pressed as of the previous tick.</summary>
- private SButton[] PreviousPressedButtons = new SButton[0];
-
- /// <summary>A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick.</summary>
- private KeyboardState PreviousKeyState;
-
- /// <summary>A record of the controller state (i.e. the up/down state for each button) as of the previous tick.</summary>
- private GamePadState PreviousControllerState;
-
- /// <summary>A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick.</summary>
- private MouseState PreviousMouseState;
-
- /// <summary>The previous mouse position on the screen adjusted for the zoom level.</summary>
- private Point PreviousMousePosition;
+ /// <summary>The player input as of the previous tick.</summary>
+ private InputState PreviousInput = new InputState();
/// <summary>The window size value at last check.</summary>
private Point PreviousWindowSize;
@@ -240,6 +229,13 @@ namespace StardewModdingAPI.Framework
return;
}
+ // game is asynchronously loading a save, block mod events to avoid conflicts
+ if (Game1.gameMode == Game1.loadingMode)
+ {
+ base.Update(gameTime);
+ return;
+ }
+
/*********
** Save events + suppress events during save
*********/
@@ -348,34 +344,17 @@ namespace StardewModdingAPI.Framework
*********/
if (Game1.game1.IsActive)
{
- // get latest state
- KeyboardState keyState;
- GamePadState controllerState;
- MouseState mouseState;
- Point mousePosition;
+ // get input state
+ InputState inputState;
try
{
- keyState = Keyboard.GetState();
- controllerState = GamePad.GetState(PlayerIndex.One);
- mouseState = Mouse.GetState();
- mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY());
+ inputState = InputState.GetState(this.PreviousInput);
}
catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true
{
- keyState = this.PreviousKeyState;
- controllerState = this.PreviousControllerState;
- mouseState = this.PreviousMouseState;
- mousePosition = this.PreviousMousePosition;
+ inputState = this.PreviousInput;
}
- // analyse state
- SButton[] currentlyPressedKeys = this.GetPressedButtons(keyState, mouseState, controllerState).ToArray();
- SButton[] previousPressedKeys = this.PreviousPressedButtons;
- SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray();
- SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray();
- bool isUseToolButton = Game1.options.useToolButton.Any(p => framePressedKeys.Contains(p.ToSButton()));
- bool isActionButton = !isUseToolButton && Game1.options.actionButton.Any(p => framePressedKeys.Contains(p.ToSButton()));
-
// get cursor position
ICursorPosition cursor;
{
@@ -388,60 +367,58 @@ namespace StardewModdingAPI.Framework
cursor = new CursorPosition(screenPixels, tile, grabTile);
}
- // raise button pressed
- foreach (SButton button in framePressedKeys)
+ // raise input events
+ foreach (var pair in inputState.ActiveButtons)
{
- InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isActionButton, isUseToolButton);
+ SButton button = pair.Key;
+ InputStatus status = pair.Value;
- // legacy events
- if (button.TryGetKeyboard(out Keys key))
- {
- if (key != Keys.None)
- ControlEvents.InvokeKeyPressed(this.Monitor, key);
- }
- else if (button.TryGetController(out Buttons controllerButton))
+ if (status == InputStatus.Pressed)
{
- if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger)
- ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right);
- else
- ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton);
- }
- }
-
- // raise button released
- foreach (SButton button in frameReleasedKeys)
- {
- bool wasUseToolButton = (from opt in Game1.options.useToolButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any();
- bool wasActionButton = !wasUseToolButton && (from opt in Game1.options.actionButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any();
- InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasActionButton, wasUseToolButton);
+ InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton());
- // legacy events
- if (button.TryGetKeyboard(out Keys key))
- {
- if (key != Keys.None)
- ControlEvents.InvokeKeyReleased(this.Monitor, key);
+ // legacy events
+ if (button.TryGetKeyboard(out Keys key))
+ {
+ if (key != Keys.None)
+ ControlEvents.InvokeKeyPressed(this.Monitor, key);
+ }
+ else if (button.TryGetController(out Buttons controllerButton))
+ {
+ if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger)
+ ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right);
+ else
+ ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton);
+ }
}
- else if (button.TryGetController(out Buttons controllerButton))
+ else if (status == InputStatus.Released)
{
- if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger)
- ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right);
- else
- ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton);
+ InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton());
+
+ // legacy events
+ if (button.TryGetKeyboard(out Keys key))
+ {
+ if (key != Keys.None)
+ ControlEvents.InvokeKeyReleased(this.Monitor, key);
+ }
+ else if (button.TryGetController(out Buttons controllerButton))
+ {
+ if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger)
+ ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right);
+ else
+ ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton);
+ }
}
}
// raise legacy state-changed events
- if (keyState != this.PreviousKeyState)
- ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousKeyState, keyState);
- if (mouseState != this.PreviousMouseState)
- ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousMouseState, mouseState, this.PreviousMousePosition, mousePosition);
+ if (inputState.KeyboardState != this.PreviousInput.KeyboardState)
+ ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousInput.KeyboardState, inputState.KeyboardState);
+ if (inputState.MouseState != this.PreviousInput.MouseState)
+ ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition);
// track state
- this.PreviousMouseState = mouseState;
- this.PreviousMousePosition = mousePosition;
- this.PreviousKeyState = keyState;
- this.PreviousControllerState = controllerState;
- this.PreviousPressedButtons = currentlyPressedKeys;
+ this.PreviousInput = inputState;
}
/*********
@@ -1304,67 +1281,7 @@ namespace StardewModdingAPI.Framework
this.PreviousSaveID = 0;
}
- /// <summary>Get the buttons pressed in the given stats.</summary>
- /// <param name="keyboard">The keyboard state.</param>
- /// <param name="mouse">The mouse state.</param>
- /// <param name="controller">The controller state.</param>
- private IEnumerable<SButton> 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)
- {
- 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.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;
- 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;
- if (controller.Triggers.Left > 0.2f)
- yield return SButton.LeftTrigger;
- if (controller.Triggers.Right > 0.2f)
- yield return SButton.RightTrigger;
- }
- }
+
/// <summary>Get the player inventory changes between two states.</summary>
/// <param name="current">The player's current inventory.</param>
diff --git a/src/SMAPI/Framework/Serialisation/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/ColorConverter.cs
new file mode 100644
index 00000000..d29f73b8
--- /dev/null
+++ b/src/SMAPI/Framework/Serialisation/ColorConverter.cs
@@ -0,0 +1,82 @@
+using System;
+using Microsoft.Xna.Framework;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using StardewModdingAPI.Framework.Exceptions;
+
+namespace StardewModdingAPI.Framework.Serialisation
+{
+ /// <summary>Handles deserialisation of <see cref="Color"/> for crossplatform compatibility.</summary>
+ internal class ColorConverter : JsonConverter
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether this converter can write JSON.</summary>
+ public override bool CanWrite => false;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether this instance can convert the specified object type.</summary>
+ /// <param name="objectType">The object type.</param>
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(Color);
+ }
+
+ /// <summary>Reads the JSON representation of the object.</summary>
+ /// <param name="reader">The JSON reader.</param>
+ /// <param name="objectType">The object type.</param>
+ /// <param name="existingValue">The object being read.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ // Linux/Mac: { "B": 76, "G": 51, "R": 25, "A": 102 }
+ // Windows: "26, 51, 76, 102"
+ JToken token = JToken.Load(reader);
+ switch (token.Type)
+ {
+ case JTokenType.Object:
+ {
+ JObject obj = (JObject)token;
+ int r = obj.Value<int>(nameof(Color.R));
+ int g = obj.Value<int>(nameof(Color.G));
+ int b = obj.Value<int>(nameof(Color.B));
+ int a = obj.Value<int>(nameof(Color.A));
+ return new Color(r, g, b, a);
+ }
+
+ case JTokenType.String:
+ {
+ string str = token.Value<string>();
+ if (string.IsNullOrWhiteSpace(str))
+ return null;
+
+ string[] parts = str.Split(',');
+ if (parts.Length != 4)
+ throw new SParseException($"Can't parse {typeof(Color).Name} from {token.Path}, invalid value '{str}'.");
+
+ int r = Convert.ToInt32(parts[0]);
+ int g = Convert.ToInt32(parts[1]);
+ int b = Convert.ToInt32(parts[2]);
+ int a = Convert.ToInt32(parts[3]);
+ return new Color(r, g, b, a);
+ }
+
+ default:
+ throw new SParseException($"Can't parse {typeof(Point).Name} from {token.Path}, must be an object or string.");
+ }
+ }
+
+ /// <summary>Writes the JSON representation of the object.</summary>
+ /// <param name="writer">The JSON writer.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new InvalidOperationException("This converter does not write JSON.");
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs
index 7c4e3ee3..f66f9dfb 100644
--- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs
+++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs
@@ -19,9 +19,15 @@ namespace StardewModdingAPI.Framework.Serialisation
ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
Converters = new List<JsonConverter>
{
+ // enums
new StringEnumConverter<Buttons>(),
new StringEnumConverter<Keys>(),
- new StringEnumConverter<SButton>()
+ new StringEnumConverter<SButton>(),
+
+ // crossplatform compatibility
+ new ColorConverter(),
+ new PointConverter(),
+ new RectangleConverter()
}
};
diff --git a/src/SMAPI/Framework/Serialisation/PointConverter.cs b/src/SMAPI/Framework/Serialisation/PointConverter.cs
new file mode 100644
index 00000000..d35660be
--- /dev/null
+++ b/src/SMAPI/Framework/Serialisation/PointConverter.cs
@@ -0,0 +1,79 @@
+using System;
+using Microsoft.Xna.Framework;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using StardewModdingAPI.Framework.Exceptions;
+
+namespace StardewModdingAPI.Framework.Serialisation
+{
+ /// <summary>Handles deserialisation of <see cref="PointConverter"/> for crossplatform compatibility.</summary>
+ internal class PointConverter : JsonConverter
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether this converter can write JSON.</summary>
+ public override bool CanWrite => false;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether this instance can convert the specified object type.</summary>
+ /// <param name="objectType">The object type.</param>
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(Point);
+ }
+
+ /// <summary>Reads the JSON representation of the object.</summary>
+ /// <param name="reader">The JSON reader.</param>
+ /// <param name="objectType">The object type.</param>
+ /// <param name="existingValue">The object being read.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ // point
+ // Linux/Mac: { "X": 1, "Y": 2 }
+ // Windows: "1, 2"
+ JToken token = JToken.Load(reader);
+ switch (token.Type)
+ {
+ case JTokenType.Object:
+ {
+ JObject obj = (JObject)token;
+ int x = obj.Value<int>(nameof(Point.X));
+ int y = obj.Value<int>(nameof(Point.Y));
+ return new Point(x, y);
+ }
+
+ case JTokenType.String:
+ {
+ string str = token.Value<string>();
+ if (string.IsNullOrWhiteSpace(str))
+ return null;
+
+ string[] parts = str.Split(',');
+ if (parts.Length != 2)
+ throw new SParseException($"Can't parse {typeof(Point).Name} from {token.Path}, invalid value '{str}'.");
+
+ int x = Convert.ToInt32(parts[0]);
+ int y = Convert.ToInt32(parts[1]);
+ return new Point(x, y);
+ }
+
+ default:
+ throw new SParseException($"Can't parse {typeof(Point).Name} from {token.Path}, must be an object or string.");
+ }
+ }
+
+ /// <summary>Writes the JSON representation of the object.</summary>
+ /// <param name="writer">The JSON writer.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new InvalidOperationException("This converter does not write JSON.");
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Serialisation/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs
new file mode 100644
index 00000000..74df54e2
--- /dev/null
+++ b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Text.RegularExpressions;
+using Microsoft.Xna.Framework;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using StardewModdingAPI.Framework.Exceptions;
+
+namespace StardewModdingAPI.Framework.Serialisation
+{
+ /// <summary>Handles deserialisation of <see cref="Rectangle"/> for crossplatform compatibility.</summary>
+ internal class RectangleConverter : JsonConverter
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether this converter can write JSON.</summary>
+ public override bool CanWrite => false;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get whether this instance can convert the specified object type.</summary>
+ /// <param name="objectType">The object type.</param>
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(Rectangle);
+ }
+
+ /// <summary>Reads the JSON representation of the object.</summary>
+ /// <param name="reader">The JSON reader.</param>
+ /// <param name="objectType">The object type.</param>
+ /// <param name="existingValue">The object being read.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ // Linux/Mac: { "X": 1, "Y": 2, "Width": 3, "Height": 4 }
+ // Windows: "{X:1 Y:2 Width:3 Height:4}"
+ JToken token = JToken.Load(reader);
+ switch (token.Type)
+ {
+ case JTokenType.Object:
+ {
+ JObject obj = (JObject)token;
+ int x = obj.Value<int>(nameof(Rectangle.X));
+ int y = obj.Value<int>(nameof(Rectangle.Y));
+ int width = obj.Value<int>(nameof(Rectangle.Width));
+ int height = obj.Value<int>(nameof(Rectangle.Height));
+ return new Rectangle(x, y, width, height);
+ }
+
+ case JTokenType.String:
+ {
+ string str = token.Value<string>();
+ if (string.IsNullOrWhiteSpace(str))
+ return Rectangle.Empty;
+
+ var match = Regex.Match(str, @"^\{X:(?<x>\d+) Y:(?<y>\d+) Width:(?<width>\d+) Height:(?<height>\d+)\}$");
+ if (!match.Success)
+ throw new SParseException($"Can't parse {typeof(Rectangle).Name} from {reader.Path}, invalid string format.");
+
+ int x = Convert.ToInt32(match.Groups["x"].Value);
+ int y = Convert.ToInt32(match.Groups["y"].Value);
+ int width = Convert.ToInt32(match.Groups["width"].Value);
+ int height = Convert.ToInt32(match.Groups["height"].Value);
+
+ return new Rectangle(x, y, width, height);
+ }
+
+ default:
+ throw new SParseException($"Can't parse {typeof(Rectangle).Name} from {reader.Path}, must be an object or string.");
+ }
+ }
+
+ /// <summary>Writes the JSON representation of the object.</summary>
+ /// <param name="writer">The JSON writer.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new InvalidOperationException("This converter does not write JSON.");
+ }
+ }
+}
diff --git a/src/SMAPI/SButton.cs b/src/SMAPI/SButton.cs
index bd6635c7..3f95169a 100644
--- a/src/SMAPI/SButton.cs
+++ b/src/SMAPI/SButton.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using Microsoft.Xna.Framework.Input;
using StardewValley;
@@ -683,5 +684,19 @@ namespace StardewModdingAPI
button = default(InputButton);
return false;
}
+
+ /// <summary>Get whether the given button is equivalent to <see cref="Options.useToolButton"/>.</summary>
+ /// <param name="input">The button.</param>
+ public static bool IsUseToolButton(this SButton input)
+ {
+ return input == SButton.ControllerX || Game1.options.useToolButton.Any(p => p.ToSButton() == input);
+ }
+
+ /// <summary>Get whether the given button is equivalent to <see cref="Options.actionButton"/>.</summary>
+ /// <param name="input">The button.</param>
+ public static bool IsActionButton(this SButton input)
+ {
+ return input == SButton.ControllerA || Game1.options.actionButton.Any(p => p.ToSButton() == input);
+ }
}
}
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index c9c302f5..e02e1ab4 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -86,6 +86,8 @@
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Framework\Content\ContentCache.cs" />
+ <Compile Include="Framework\Input\InputState.cs" />
+ <Compile Include="Framework\Input\InputStatus.cs" />
<Compile Include="Framework\LegacyManifestVersion.cs" />
<Compile Include="Framework\Models\ModCompatibility.cs" />
<Compile Include="Framework\ModLoading\Finders\EventFinder.cs" />
@@ -108,6 +110,9 @@
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
+ <Compile Include="Framework\Serialisation\RectangleConverter.cs" />
+ <Compile Include="Framework\Serialisation\ColorConverter.cs" />
+ <Compile Include="Framework\Serialisation\PointConverter.cs" />
<Compile Include="Framework\Utilities\ContextHash.cs" />
<Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" />