Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2018-01-24 11:44:28 -05:00
commit 0da5dab893
38 changed files with 867 additions and 307 deletions

View File

@ -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")]

View File

@ -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).

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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" />

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -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;

View File

@ -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>

View File

@ -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");

View File

@ -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)

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
)
{ }
}
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton());
// 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);
// 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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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 error = $"Can't parse JSON file at {fullPath}.";
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.";
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;
}
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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}).");
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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" />

View File

@ -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>

View File

@ -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