summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/GlobalAssemblyInfo.cs4
-rw-r--r--docs/release-notes.md25
-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
38 files changed, 868 insertions, 308 deletions
diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs
index bcfdc124..79d473e6 100644
--- a/build/GlobalAssemblyInfo.cs
+++ b/build/GlobalAssemblyInfo.cs
@@ -2,5 +2,5 @@ using System.Reflection;
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
-[assembly: AssemblyVersion("2.3.0.0")]
-[assembly: AssemblyFileVersion("2.3.0.0")]
+[assembly: AssemblyVersion("2.4.0.0")]
+[assembly: AssemblyFileVersion("2.4.0.0")]
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 165e7d4e..9c1fbf93 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,4 +1,29 @@
# Release notes
+## 2.4
+* For players:
+ * Fixed visual map glitch in rare cases.
+ * Fixed error parsing JSON files which have curly quotes.
+ * Fixed error parsing some JSON files generated on another system.
+ * Fixed error parsing some JSON files after mods reload core assemblies, which is no longer allowed.
+ * Fixed intermittent errors (e.g. 'collection has been modified') with some mods when loading a save.
+ * Fixed compatibility with Linux Terminator terminal.
+
+* For the [log parser][]:
+ * Fixed error parsing logs with zero installed mods.
+
+* For modders:
+ * Added `SaveEvents.BeforeCreate` and `AfterCreate` events.
+ * Added `SButton` `IsActionButton()` and `IsUseToolButton()` extensions.
+ * Improved JSON parse errors to provide more useful info for troubleshooting.
+ * Fixed events being raised while the game is loading a save file.
+ * Fixed input events not recognising controller input as an action or use-tool button.
+ * Fixed input events setting the same `IsActionButton` and `IsUseToolButton` values for all buttons pressed in an update tick.
+ * Fixed semantic versions ignoring `-0` as a prerelease tag.
+ * Updated Json.NET to 11.0.1-beta3 (needed to avoid a parser edge case).
+
+* For SMAPI developers:
+ * Overhauled input handling to support future input events.
+
## 2.3
* For players:
* Added a user-friendly [download page](https://smapi.io).
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&