summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-01-24 11:44:28 -0500
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-01-24 11:44:28 -0500
commit0da5dab8932c53ba39f2290268294e9a72b9c5bb (patch)
tree9302a2a53a68202ac97e86662394a509df499541 /src
parent15d4b6310e3dd15c62f3faedbf1290b2db26fb59 (diff)
parent5c96a10da5801049ee17ffa185dbf19e6d8a2306 (diff)
downloadSMAPI-0da5dab8932c53ba39f2290268294e9a72b9c5bb.tar.gz
SMAPI-0da5dab8932c53ba39f2290268294e9a72b9c5bb.tar.bz2
SMAPI-0da5dab8932c53ba39f2290268294e9a72b9c5bb.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Common/SemanticVersionImpl.cs4
-rw-r--r--src/SMAPI.ModBuildConfig/build/smapi.targets4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json7
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/packages.config2
-rw-r--r--src/SMAPI.Tests/StardewModdingAPI.Tests.csproj4
-rw-r--r--src/SMAPI.Tests/Utilities/SemanticVersionTests.cs17
-rw-r--r--src/SMAPI.Tests/packages.config2
-rw-r--r--src/SMAPI.Web/Views/Index/Index.cshtml2
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml4
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/log-parser.js2
-rw-r--r--src/SMAPI.sln.DotSettings6
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Events/SaveEvents.cs22
-rw-r--r--src/SMAPI/Framework/Input/InputState.cs163
-rw-r--r--src/SMAPI/Framework/Input/InputStatus.cs29
-rw-r--r--src/SMAPI/Framework/LegacyManifestVersion.cs26
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs4
-rw-r--r--src/SMAPI/Framework/Models/Manifest.cs8
-rw-r--r--src/SMAPI/Framework/Models/ModDataRecord.cs6
-rw-r--r--src/SMAPI/Framework/SGame.cs211
-rw-r--r--src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs46
-rw-r--r--src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs42
-rw-r--r--src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs51
-rw-r--r--src/SMAPI/Framework/Serialisation/JsonHelper.cs59
-rw-r--r--src/SMAPI/Framework/Serialisation/SFieldConverter.cs121
-rw-r--r--src/SMAPI/Framework/Serialisation/SimpleReadOnlyConverter.cs77
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs60
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs61
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs19
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs36
-rw-r--r--src/SMAPI/Framework/Serialisation/SmapiConverters/StringEnumConverter.cs (renamed from src/SMAPI/Framework/Serialisation/StringEnumConverter.cs)2
-rw-r--r--src/SMAPI/SButton.cs15
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj18
-rw-r--r--src/SMAPI/packages.config2
-rw-r--r--src/SMAPI/unix-launcher.sh9
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">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</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