diff options
Diffstat (limited to 'src/SMAPI/Framework')
18 files changed, 744 insertions, 277 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/LegacyManifestVersion.cs b/src/SMAPI/Framework/LegacyManifestVersion.cs new file mode 100644 index 00000000..454b9137 --- /dev/null +++ b/src/SMAPI/Framework/LegacyManifestVersion.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework +{ + /// <summary>An implementation of <see cref="ISemanticVersion"/> that hamdles the legacy <see cref="IManifest"/> version format.</summary> + internal class LegacyManifestVersion : SemanticVersion + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="majorVersion">The major version incremented for major API changes.</param> + /// <param name="minorVersion">The minor version incremented for backwards-compatible changes.</param> + /// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param> + /// <param name="build">An optional build tag.</param> + [JsonConstructor] + public LegacyManifestVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) + : base( + majorVersion, + minorVersion, + patchVersion, + build != "0" ? build : null // '0' from incorrect examples in old SMAPI documentation + ) + { } + } +} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 1e3c4a05..3a7b214a 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -162,7 +162,11 @@ namespace StardewModdingAPI.Framework.ModLoading // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) + { yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); + yield break; + } + visitedAssemblyNames.Add(assembly.Name.Name); // yield referenced assemblies diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index b85787e5..f9762406 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Serialisation.SmapiConverters; namespace StardewModdingAPI.Framework.Models { @@ -20,18 +20,18 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// <summary>The mod version.</summary> - [JsonConverter(typeof(SFieldConverter))] + [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion Version { get; set; } /// <summary>The minimum SMAPI version required by this mod, if any.</summary> - [JsonConverter(typeof(SFieldConverter))] + [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method.</summary> public string EntryDll { get; set; } /// <summary>The other mods that must be loaded before this mod.</summary> - [JsonConverter(typeof(SFieldConverter))] + [JsonConverter(typeof(ManifestDependencyArrayConverter))] public IManifestDependency[] Dependencies { get; set; } /// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary> diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs index c6a12188..580acb70 100644 --- a/src/SMAPI/Framework/Models/ModDataRecord.cs +++ b/src/SMAPI/Framework/Models/ModDataRecord.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Serialisation.SmapiConverters; namespace StardewModdingAPI.Framework.Models { @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Framework.Models ** Accessors *********/ /// <summary>The unique mod identifier.</summary> - [JsonConverter(typeof(SFieldConverter))] + [JsonConverter(typeof(ModDataIdConverter))] public ModDataID ID { get; set; } /// <summary>A value to inject into <see cref="IManifest.UpdateKeys"/> field if it's not already set.</summary> @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.Models public string AlternativeUrl { get; set; } /// <summary>The compatibility of given mod versions (if any).</summary> - [JsonConverter(typeof(SFieldConverter))] + [JsonConverter(typeof(ModCompatibilityArrayConverter))] public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; /// <summary>Map local versions to a semantic version for update checks.</summary> diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 0a614f17..e82ee778 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; @@ -50,23 +51,14 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the game is saving and SMAPI has already raised <see cref="SaveEvents.BeforeSave"/>.</summary> private bool IsBetweenSaveEvents; + /// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="SaveEvents.BeforeCreate"/>.</summary> + private bool IsBetweenCreateEvents; + /**** ** 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 +232,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 *********/ @@ -250,6 +249,14 @@ namespace StardewModdingAPI.Framework // opened (since the save hasn't started yet), but all other events should be suppressed. if (Context.IsSaving) { + // raise before-create + if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) + { + this.IsBetweenCreateEvents = true; + this.Monitor.Log("Context: before save creation.", LogLevel.Trace); + SaveEvents.InvokeBeforeCreate(this.Monitor); + } + // raise before-save if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { @@ -262,6 +269,13 @@ namespace StardewModdingAPI.Framework base.Update(gameTime); return; } + if (this.IsBetweenCreateEvents) + { + // raise after-create + this.IsBetweenCreateEvents = false; + this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + SaveEvents.InvokeAfterCreated(this.Monitor); + } if (this.IsBetweenSaveEvents) { // raise after-save @@ -348,34 +362,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 +385,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 (status == InputStatus.Pressed) { - 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 ? 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 +1299,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/CrossplatformConverters/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs new file mode 100644 index 00000000..f4a2a26e --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; + +namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters +{ + /// <summary>Handles deserialisation of <see cref="Color"/> for crossplatform compatibility.</summary> + /// <remarks> + /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } + /// - Windows format: "26, 51, 76, 102" + /// </remarks> + internal class ColorConverter : SimpleReadOnlyConverter<Color> + { + /********* + ** Protected methods + *********/ + /// <summary>Read a JSON object.</summary> + /// <param name="obj">The JSON object to read.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override Color ReadObject(JObject obj, string path) + { + 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); + } + + /// <summary>Read a JSON string.</summary> + /// <param name="str">The JSON string value.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override Color ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 4) + throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); + + 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); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs new file mode 100644 index 00000000..84c70989 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; + +namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters +{ + /// <summary>Handles deserialisation of <see cref="PointConverter"/> for crossplatform compatibility.</summary> + /// <remarks> + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// </remarks> + internal class PointConverter : SimpleReadOnlyConverter<Point> + { + /********* + ** Protected methods + *********/ + /// <summary>Read a JSON object.</summary> + /// <param name="obj">The JSON object to read.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override Point ReadObject(JObject obj, string path) + { + int x = obj.Value<int>(nameof(Point.X)); + int y = obj.Value<int>(nameof(Point.Y)); + return new Point(x, y); + } + + /// <summary>Read a JSON string.</summary> + /// <param name="str">The JSON string value.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override Point ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(parts[0]); + int y = Convert.ToInt32(parts[1]); + return new Point(x, y); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs new file mode 100644 index 00000000..b89551e3 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; + +namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters +{ + /// <summary>Handles deserialisation of <see cref="Rectangle"/> for crossplatform compatibility.</summary> + /// <remarks> + /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } + /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" + /// </remarks> + internal class RectangleConverter : SimpleReadOnlyConverter<Rectangle> + { + /********* + ** Protected methods + *********/ + /// <summary>Read a JSON object.</summary> + /// <param name="obj">The JSON object to read.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override Rectangle ReadObject(JObject obj, string path) + { + 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); + } + + /// <summary>Read a JSON string.</summary> + /// <param name="str">The JSON string value.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override Rectangle ReadString(string str, string path) + { + 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 invalid value '{str}' (path: {path})."); + + 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); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index d923ec0c..2e2a666e 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation.CrossplatformConverters; +using StardewModdingAPI.Framework.Serialisation.SmapiConverters; namespace StardewModdingAPI.Framework.Serialisation { @@ -19,9 +21,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() } }; @@ -55,18 +63,20 @@ namespace StardewModdingAPI.Framework.Serialisation // deserialise model try { - return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings); + return this.Deserialise<TModel>(json); } - catch (JsonReaderException ex) + catch (Exception ex) { - string message = $"The file at {fullPath} doesn't seem to be valid JSON."; - - string text = File.ReadAllText(fullPath); - if (text.Contains("“") || text.Contains("”")) - message += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + string error = $"Can't parse JSON file at {fullPath}."; - message += $"\nTechnical details: {ex.Message}"; - throw new JsonReaderException(message); + if (ex is JsonReaderException) + { + error += " This doesn't seem to be valid JSON."; + if (json.Contains("“") || json.Contains("”")) + error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + } + error += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(error); } } @@ -93,5 +103,34 @@ namespace StardewModdingAPI.Framework.Serialisation string json = JsonConvert.SerializeObject(model, this.JsonSettings); File.WriteAllText(fullPath, json); } + + + /********* + ** Private methods + *********/ + /// <summary>Deserialize JSON text if possible.</summary> + /// <typeparam name="TModel">The model type.</typeparam> + /// <param name="json">The raw JSON text.</param> + private TModel Deserialise<TModel>(string json) + { + try + { + return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings); + } + catch (JsonReaderException) + { + // try replacing curly quotes + if (json.Contains("“") || json.Contains("”")) + { + try + { + return JsonConvert.DeserializeObject<TModel>(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); + } + catch { /* rethrow original error */ } + } + + throw; + } + } } } diff --git a/src/SMAPI/Framework/Serialisation/SFieldConverter.cs b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs deleted file mode 100644 index 917c950d..00000000 --- a/src/SMAPI/Framework/Serialisation/SFieldConverter.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// <summary>Overrides how SMAPI reads and writes <see cref="ISemanticVersion"/> and <see cref="IManifestDependency"/> fields.</summary> - internal class SFieldConverter : 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(ISemanticVersion) - || objectType == typeof(IManifestDependency[]) - || objectType == typeof(ModDataID) - || objectType == typeof(ModCompatibility[]); - } - - /// <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) - { - // semantic version - if (objectType == typeof(ISemanticVersion)) - { - JToken token = JToken.Load(reader); - switch (token.Type) - { - case JTokenType.Object: - { - JObject obj = (JObject)token; - int major = obj.Value<int>(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.Value<int>(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.Value<int>(nameof(ISemanticVersion.PatchVersion)); - string build = obj.Value<string>(nameof(ISemanticVersion.Build)); - return new SemanticVersion(major, minor, patch, build); - } - - case JTokenType.String: - { - string str = token.Value<string>(); - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) - throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta."); - return version; - } - - default: - throw new SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); - } - } - - // manifest dependencies - if (objectType == typeof(IManifestDependency[])) - { - List<IManifestDependency> result = new List<IManifestDependency>(); - foreach (JObject obj in JArray.Load(reader).Children<JObject>()) - { - string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID)); - string minVersion = obj.Value<string>(nameof(IManifestDependency.MinimumVersion)); - bool required = obj.Value<bool?>(nameof(IManifestDependency.IsRequired)) ?? true; - result.Add(new ManifestDependency(uniqueID, minVersion, required)); - } - return result.ToArray(); - } - - // mod data ID - if (objectType == typeof(ModDataID)) - { - JToken token = JToken.Load(reader); - return new ModDataID(token.Value<string>()); - } - - // mod compatibility records - if (objectType == typeof(ModCompatibility[])) - { - List<ModCompatibility> result = new List<ModCompatibility>(); - foreach (JProperty property in JObject.Load(reader).Properties()) - { - string range = property.Name; - ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value<string>(nameof(ModCompatibility.Status))); - string reasonPhrase = property.Value.Value<string>(nameof(ModCompatibility.ReasonPhrase)); - - result.Add(new ModCompatibility(range, status, reasonPhrase)); - } - return result.ToArray(); - } - - // unknown - throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); - } - - /// <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/SimpleReadOnlyConverter.cs b/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs new file mode 100644 index 00000000..5765ad96 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs @@ -0,0 +1,77 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// <summary>The base implementation for simplified converters which deserialise <typeparamref name="T"/> without overriding serialisation.</summary> + /// <typeparam name="T">The type to deserialise.</typeparam> + internal abstract class SimpleReadOnlyConverter<T> : 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(T); + } + + /// <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."); + } + + /// <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) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader), path); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value<string>(), path); + default: + throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + + /********* + ** Protected methods + *********/ + /// <summary>Read a JSON object.</summary> + /// <param name="obj">The JSON object to read.</param> + /// <param name="path">The path to the current JSON node.</param> + protected virtual T ReadObject(JObject obj, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); + } + + /// <summary>Read a JSON string.</summary> + /// <param name="str">The JSON string value.</param> + /// <param name="path">The path to the current JSON node.</param> + protected virtual T ReadString(string str, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs new file mode 100644 index 00000000..6352e367 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters +{ + /// <summary>Handles deserialisation of <see cref="IManifestDependency"/> arrays.</summary> + internal class ManifestDependencyArrayConverter : 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(IManifestDependency[]); + } + + + /********* + ** Protected methods + *********/ + /// <summary>Read 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) + { + List<IManifestDependency> result = new List<IManifestDependency>(); + foreach (JObject obj in JArray.Load(reader).Children<JObject>()) + { + string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID)); + string minVersion = obj.Value<string>(nameof(IManifestDependency.MinimumVersion)); + bool required = obj.Value<bool?>(nameof(IManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); + } + return result.ToArray(); + } + + /// <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/SmapiConverters/ModCompatibilityArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs new file mode 100644 index 00000000..3232dde4 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters +{ + /// <summary>Handles deserialisation of <see cref="ModCompatibility"/> arrays.</summary> + internal class ModCompatibilityArrayConverter : 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(ModCompatibility[]); + } + + + /********* + ** Protected methods + *********/ + /// <summary>Read 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) + { + List<ModCompatibility> result = new List<ModCompatibility>(); + foreach (JProperty property in JObject.Load(reader).Properties()) + { + string range = property.Name; + ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value<string>(nameof(ModCompatibility.Status))); + string reasonPhrase = property.Value.Value<string>(nameof(ModCompatibility.ReasonPhrase)); + + result.Add(new ModCompatibility(range, status, reasonPhrase)); + } + return result.ToArray(); + } + + /// <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/SmapiConverters/ModDataIdConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs new file mode 100644 index 00000000..8a10db47 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs @@ -0,0 +1,19 @@ +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters +{ + /// <summary>Handles deserialisation of <see cref="ModDataID"/>.</summary> + internal class ModDataIdConverter : SimpleReadOnlyConverter<ModDataID> + { + /********* + ** Protected methods + *********/ + /// <summary>Read a JSON string.</summary> + /// <param name="str">The JSON string value.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override ModDataID ReadString(string str, string path) + { + return new ModDataID(str); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs new file mode 100644 index 00000000..50181809 --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; + +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters +{ + /// <summary>Handles deserialisation of <see cref="SemanticVersion"/>.</summary> + internal class SemanticVersionConverter : SimpleReadOnlyConverter<ISemanticVersion> + { + /********* + ** Protected methods + *********/ + /// <summary>Read a JSON object.</summary> + /// <param name="obj">The JSON object to read.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override ISemanticVersion ReadObject(JObject obj, string path) + { + int major = obj.Value<int>(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.Value<int>(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.Value<int>(nameof(ISemanticVersion.PatchVersion)); + string build = obj.Value<string>(nameof(ISemanticVersion.Build)); + return new LegacyManifestVersion(major, minor, patch, build); + } + + /// <summary>Read a JSON string.</summary> + /// <param name="str">The JSON string value.</param> + /// <param name="path">The path to the current JSON node.</param> + protected override ISemanticVersion ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); + return version; + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs index 7afe86cd..c88ac834 100644 --- a/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs @@ -1,7 +1,7 @@ using System; using Newtonsoft.Json.Converters; -namespace StardewModdingAPI.Framework.Serialisation +namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters { /// <summary>A variant of <see cref="StringEnumConverter"/> which only converts a specified enum.</summary> /// <typeparam name="T">The enum type.</typeparam> |