diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-01-24 11:44:28 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-01-24 11:44:28 -0500 |
commit | 0da5dab8932c53ba39f2290268294e9a72b9c5bb (patch) | |
tree | 9302a2a53a68202ac97e86662394a509df499541 /src | |
parent | 15d4b6310e3dd15c62f3faedbf1290b2db26fb59 (diff) | |
parent | 5c96a10da5801049ee17ffa185dbf19e6d8a2306 (diff) | |
download | SMAPI-0da5dab8932c53ba39f2290268294e9a72b9c5bb.tar.gz SMAPI-0da5dab8932c53ba39f2290268294e9a72b9c5bb.tar.bz2 SMAPI-0da5dab8932c53ba39f2290268294e9a72b9c5bb.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src')
36 files changed, 841 insertions, 306 deletions
diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs index 53cf5a21..1c713b47 100644 --- a/src/SMAPI.Common/SemanticVersionImpl.cs +++ b/src/SMAPI.Common/SemanticVersionImpl.cs @@ -190,9 +190,7 @@ namespace StardewModdingAPI.Common private string GetNormalisedTag(string tag) { tag = tag?.Trim(); - if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation - return null; - return tag; + return !string.IsNullOrWhiteSpace(tag) ? tag : null; } } } diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index c0319e22..83f0dcbd 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -122,8 +122,8 @@ <Error Condition="'$(OS)' != 'OSX' AND '$(OS)' != 'Unix' AND '$(OS)' != 'Windows_NT'" Text="The mod build package doesn't recognise OS type '$(OS)'." /> <Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see details at https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#game-path." /> - <Error Condition="'$(OS)' == 'Windows_NT' AND !Exists('$(GamePath)\Stardew Valley.exe')" Text="The mod build package found a a game folder at $(GamePath), but it doesn't contain the Stardew Valley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." /> - <Error Condition="'$(OS)' != 'Windows_NT' AND !Exists('$(GamePath)\StardewValley.exe')" Text="The mod build package found a a game folder at $(GamePath), but it doesn't contain the StardewValley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." /> + <Error Condition="'$(OS)' == 'Windows_NT' AND !Exists('$(GamePath)\Stardew Valley.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the Stardew Valley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." /> + <Error Condition="'$(OS)' != 'Windows_NT' AND !Exists('$(GamePath)\StardewValley.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the StardewValley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." /> <Error Condition="!Exists('$(GamePath)\StardewModdingAPI.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." /> </Target> diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index a65ad72c..a5b89a33 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -36,8 +36,8 @@ <PlatformTarget>x86</PlatformTarget> </PropertyGroup> <ItemGroup> - <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> + <Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.11.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath> <Private>False</Private> </Reference> <Reference Include="System" /> diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 06ab1b54..fc5ce35d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,12 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": { - "MajorVersion": 2, - "MinorVersion": 3, - "PatchVersion": 0, - "Build": null - }, + "Version": "2.4.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.ConsoleCommands/packages.config b/src/SMAPI.Mods.ConsoleCommands/packages.config index ee51c237..a0f76c34 100644 --- a/src/SMAPI.Mods.ConsoleCommands/packages.config +++ b/src/SMAPI.Mods.ConsoleCommands/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" /> + <package id="Newtonsoft.Json" version="11.0.1-beta3" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index c7a67306..6e7fa1f0 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -36,8 +36,8 @@ <Reference Include="Moq, Version=4.7.142.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> <HintPath>..\packages\Moq.4.7.142\lib\net45\Moq.dll</HintPath> </Reference> - <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> + <Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.11.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> <HintPath>..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath> diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index d3e0988e..f1a72012 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -33,6 +33,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] [TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] + [TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3-0")] [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] public string Constructor_FromParts(int major, int minor, int patch, string tag) @@ -270,6 +271,22 @@ namespace StardewModdingAPI.Tests.Utilities Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); } + /**** + ** LegacyManifestVersion + ****/ + [Test(Description = "Assert that the LegacyManifestVersion subclass correctly parses legacy manifest versions.")] + [TestCase(1, 0, 0, null, ExpectedResult = "1.0")] + [TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")] + [TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")] + [TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")] + [TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3")] // special case: drop '0' tag for legacy manifest versions + [TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")] + [TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")] + public string LegacyManifestVersion(int major, int minor, int patch, string tag) + { + return new LegacyManifestVersion(major, minor, patch, tag).ToString(); + } + /********* ** Private methods diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config index 7a91e807..498325d6 100644 --- a/src/SMAPI.Tests/packages.config +++ b/src/SMAPI.Tests/packages.config @@ -2,6 +2,6 @@ <packages> <package id="Castle.Core" version="4.2.1" targetFramework="net45" /> <package id="Moq" version="4.7.142" targetFramework="net45" /> - <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" /> + <package id="Newtonsoft.Json" version="11.0.1-beta3" targetFramework="net45" /> <package id="NUnit" version="3.8.1" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 2d76cc15..ad58898e 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -62,7 +62,7 @@ <h2>For mod creators</h2> <ul> - <li><a href="@Model.DevDownloadUrl">SMAPI 2.2 for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li> + <li><a href="@Model.DevDownloadUrl">SMAPI @Model.LatestVersion for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li> <li><a href="https://stardewvalleywiki.com/Modding:Index">Modding documentation</a></li> <li>Need help? Come <a href="https://stardewvalleywiki.com/Modding:Community#Discord">chat on Discord</a>.</li> </ul> diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index b7724c69..1659de8f 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -3,9 +3,9 @@ } @model StardewModdingAPI.Web.ViewModels.LogParserModel @section Head { - <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20171202" /> + <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180101" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script> - <script src="~/Content/js/log-parser.js?r=20171202"></script> + <script src="~/Content/js/log-parser.js?r=20180101"></script> <style type="text/css" id="modflags"></style> <script> $(function() { diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 6cce1ce9..914863f6 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -175,7 +175,7 @@ smapi.logParser = function(sectionUrl, pasteID) { } var dataInfo = regexInfo.exec(data) || regexInfo.exec(data) || regexInfo.exec(data), - dataMods = regexMods.exec(data) || regexMods.exec(data) || regexMods.exec(data), + dataMods = regexMods.exec(data) || regexMods.exec(data) || regexMods.exec(data) || [""], dataDate = regexDate.exec(data) || regexDate.exec(data) || regexDate.exec(data), dataPath = regexPath.exec(data) || regexPath.exec(data) || regexPath.exec(data), match; diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index d16ef684..68d7c72b 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -6,6 +6,7 @@ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/StaticQualifier/STATIC_MEMBERS_QUALIFY_MEMBERS/@EntryValue">Field, Property, Event, Method</s:String> <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">Field, Property, Event, Method</s:String> <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/LINE_FEED_AT_FILE_END/@EntryValue">True</s:Boolean> + <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String> <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue">False</s:Boolean> <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseVarWhenEvident</s:String> <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String> @@ -13,7 +14,12 @@ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></s:String> + <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean> </wpf:ResourceDictionary>
\ No newline at end of file diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 786ae32b..515e9870 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.3"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.4.0"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); diff --git a/src/SMAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs index 50e6d729..99b6c8d2 100644 --- a/src/SMAPI/Events/SaveEvents.cs +++ b/src/SMAPI/Events/SaveEvents.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework; namespace StardewModdingAPI.Events @@ -9,6 +9,12 @@ namespace StardewModdingAPI.Events /********* ** Events *********/ + /// <summary>Raised before the game creates the save file.</summary> + public static event EventHandler BeforeCreate; + + /// <summary>Raised after the game finishes creating the save file.</summary> + public static event EventHandler AfterCreate; + /// <summary>Raised before the game begins writes data to the save file.</summary> public static event EventHandler BeforeSave; @@ -25,6 +31,20 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ + /// <summary>Raise a <see cref="BeforeCreate"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeBeforeCreate(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeCreate)}", SaveEvents.BeforeCreate?.GetInvocationList(), null, EventArgs.Empty); + } + + /// <summary>Raise a <see cref="AfterCreate"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeAfterCreated(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterCreate)}", SaveEvents.AfterCreate?.GetInvocationList(), null, EventArgs.Empty); + } + /// <summary>Raise a <see cref="BeforeSave"/> event.</summary> /// <param name="monitor">Encapsulates monitoring and logging.</param> internal static void InvokeBeforeSave(IMonitor monitor) 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> 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 f76ac439..cd6c4ac3 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -65,8 +65,8 @@ <HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath> <Private>True</Private> </Reference> - <Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> - <HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> + <Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.11.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -86,6 +86,9 @@ <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" /> <Compile Include="Framework\ModLoading\Finders\FieldFinder.cs" /> @@ -107,6 +110,14 @@ <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> <Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" /> + <Compile Include="Framework\Serialisation\SmapiConverters\ModCompatibilityArrayConverter.cs" /> + <Compile Include="Framework\Serialisation\SmapiConverters\ManifestDependencyArrayConverter.cs" /> + <Compile Include="Framework\Serialisation\SmapiConverters\ModDataIdConverter.cs" /> + <Compile Include="Framework\Serialisation\SmapiConverters\SemanticVersionConverter.cs" /> + <Compile Include="Framework\Serialisation\SimpleReadOnlyConverter.cs" /> + <Compile Include="Framework\Serialisation\CrossplatformConverters\RectangleConverter.cs" /> + <Compile Include="Framework\Serialisation\CrossplatformConverters\ColorConverter.cs" /> + <Compile Include="Framework\Serialisation\CrossplatformConverters\PointConverter.cs" /> <Compile Include="Framework\Utilities\ContextHash.cs" /> <Compile Include="IReflectedField.cs" /> <Compile Include="IReflectedMethod.cs" /> @@ -174,8 +185,7 @@ <Compile Include="Framework\SContentManager.cs" /> <Compile Include="Framework\Exceptions\SParseException.cs" /> <Compile Include="Framework\Serialisation\JsonHelper.cs" /> - <Compile Include="Framework\Serialisation\StringEnumConverter.cs" /> - <Compile Include="Framework\Serialisation\SFieldConverter.cs" /> + <Compile Include="Framework\Serialisation\SmapiConverters\StringEnumConverter.cs" /> <Compile Include="IAssetEditor.cs" /> <Compile Include="IAssetInfo.cs" /> <Compile Include="IAssetLoader.cs" /> diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 98d742c7..1a0b78fa 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <packages> <package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" /> - <package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" /> + <package id="Newtonsoft.Json" version="11.0.1-beta3" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/src/SMAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh index 70f1873a..2542a286 100644 --- a/src/SMAPI/unix-launcher.sh +++ b/src/SMAPI/unix-launcher.sh @@ -63,7 +63,14 @@ else # open SMAPI in terminal if $COMMAND x-terminal-emulator 2>/dev/null; then - x-terminal-emulator -e "$LAUNCHER" + # Terminator converts -e to -x when used through x-terminal-emulator for some reason (per + # `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator + # is mapped to Terminator, invoke it directly instead. + if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then + terminator -e "$LAUNCHER" + else + x-terminal-emulator -e "$LAUNCHER" + fi elif $COMMAND xfce4-terminal 2>/dev/null; then xfce4-terminal -e "$LAUNCHER" elif $COMMAND gnome-terminal 2>/dev/null; then |