diff options
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Framework/Input/InputState.cs | 163 | ||||
-rw-r--r-- | src/SMAPI/Framework/Input/InputStatus.cs | 29 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 193 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/ColorConverter.cs | 82 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/JsonHelper.cs | 8 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/PointConverter.cs | 79 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/RectangleConverter.cs | 84 | ||||
-rw-r--r-- | src/SMAPI/SButton.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 5 |
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" /> |