move manifest parsing into toolkit (#532)

This commit is contained in:
Jesse Plamondon-Willard 2018-06-05 20:22:46 -04:00
parent 265ce35fd1
commit 625c538f24
27 changed files with 343 additions and 189 deletions

View File

@ -9,7 +9,7 @@ using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModData;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Framework.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation;
namespace StardewModdingAPI.Tests.Core
{
@ -93,8 +93,8 @@ namespace StardewModdingAPI.Tests.Core
Assert.IsNotNull(mod, "The loaded manifest shouldn't be null.");
Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one.");
Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match.");
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded.");
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
Assert.AreEqual(original[nameof(IManifest.Name)], mod.DisplayName, "The display name should use the manifest name.");
Assert.AreEqual(original[nameof(IManifest.Name)], mod.Manifest.Name, "The manifest's name doesn't match.");
@ -160,7 +160,7 @@ namespace StardewModdingAPI.Tests.Core
{
// arrange
Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1")));
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
this.SetupMetadataForValidation(mock);
// act
@ -174,7 +174,7 @@ namespace StardewModdingAPI.Tests.Core
public void ValidateManifests_MissingEntryDLL_Fails()
{
// arrange
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.EntryDll = "Missing.dll"), allowStatusChange: true);
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true);
this.SetupMetadataForValidation(mock);
// act
@ -189,7 +189,7 @@ namespace StardewModdingAPI.Tests.Core
{
// arrange
Mock<IModMetadata> modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod A", "1.0", manifest => manifest.Name = "Mod B"), allowStatusChange: true);
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
Mock<IModMetadata> modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false);
foreach (Mock<IModMetadata> mod in new[] { modA, modB, modC })
this.SetupMetadataForValidation(mod);
@ -398,8 +398,8 @@ namespace StardewModdingAPI.Tests.Core
{
// arrange
// A 1.0 ◀── B (need A 1.1)
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true);
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.1") }), allowStatusChange: true);
// act
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray();
@ -414,8 +414,8 @@ namespace StardewModdingAPI.Tests.Core
{
// arrange
// A 1.0 ◀── B (need A 1.0-beta)
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false);
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0-beta") }), allowStatusChange: false);
// act
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }, new ModDatabase()).ToArray();
@ -431,8 +431,8 @@ namespace StardewModdingAPI.Tests.Core
{
// arrange
// A ◀── B
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest("Mod A", "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false);
Mock<IModMetadata> modA = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0"));
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false);
// act
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }, new ModDatabase()).ToArray();
@ -448,7 +448,7 @@ namespace StardewModdingAPI.Tests.Core
{
// arrange
// A ◀── B where A doesn't exist
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false);
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod B", version: "1.0", dependencies: new IManifestDependency[] { new ManifestDependency("Mod A", "1.0", required: false) }), allowStatusChange: false);
// act
IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }, new ModDatabase()).ToArray();
@ -463,46 +463,26 @@ namespace StardewModdingAPI.Tests.Core
** Private methods
*********/
/// <summary>Get a randomised basic manifest.</summary>
/// <param name="adjust">Adjust the generated manifest.</param>
private Manifest GetManifest(Action<Manifest> adjust = null)
/// <param name="id">The <see cref="IManifest.UniqueID"/> value, or <c>null</c> for a generated value.</param>
/// <param name="name">The <see cref="IManifest.Name"/> value, or <c>null</c> for a generated value.</param>
/// <param name="version">The <see cref="IManifest.Version"/> value, or <c>null</c> for a generated value.</param>
/// <param name="entryDll">The <see cref="IManifest.EntryDll"/> value, or <c>null</c> for a generated value.</param>
/// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param>
/// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param>
/// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param>
private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null)
{
Manifest manifest = new Manifest
{
Name = Sample.String(),
Author = Sample.String(),
Version = new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
Description = Sample.String(),
UniqueID = $"{Sample.String()}.{Sample.String()}",
EntryDll = $"{Sample.String()}.dll"
};
adjust?.Invoke(manifest);
return manifest;
}
/// <summary>Get a randomised basic manifest.</summary>
/// <param name="uniqueID">The mod's name and unique ID.</param>
/// <param name="version">The mod version.</param>
/// <param name="adjust">Adjust the generated manifest.</param>
/// <param name="dependencies">The dependencies this mod requires.</param>
private IManifest GetManifest(string uniqueID, string version, Action<Manifest> adjust, params IManifestDependency[] dependencies)
{
return this.GetManifest(manifest =>
{
manifest.Name = uniqueID;
manifest.UniqueID = uniqueID;
manifest.Version = new SemanticVersion(version);
manifest.Dependencies = dependencies;
adjust?.Invoke(manifest);
});
}
/// <summary>Get a randomised basic manifest.</summary>
/// <param name="uniqueID">The mod's name and unique ID.</param>
/// <param name="version">The mod version.</param>
/// <param name="dependencies">The dependencies this mod requires.</param>
private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies)
{
return this.GetManifest(uniqueID, version, null, dependencies);
return new Manifest(
uniqueID: id ?? $"{Sample.String()}.{Sample.String()}",
name: name ?? id ?? Sample.String(),
author: Sample.String(),
description: Sample.String(),
version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
entryDll: entryDll ?? $"{Sample.String()}.dll",
contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID) : null,
minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null,
dependencies: dependencies
);
}
/// <summary>Get a randomised basic manifest.</summary>
@ -518,7 +498,7 @@ namespace StardewModdingAPI.Tests.Core
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false)
{
IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray());
IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray());
return this.GetMetadata(manifest, allowStatusChange);
}

View File

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using NUnit.Framework;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.Tests.Utilities
{

View File

@ -2,7 +2,7 @@ using System;
using System.IO;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Utilities;
using xTile;

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Xna.Framework.Graphics;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
@ -91,20 +90,5 @@ namespace StardewModdingAPI.Framework
// get result
return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue();
}
/****
** Json.NET
****/
/// <summary>Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="obj">The JSON object to search.</param>
/// <param name="fieldName">The field name.</param>
public static T ValueIgnoreCase<T>(this JObject obj, string fieldName)
{
JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase);
return token != null
? token.Value<T>()
: default(T);
}
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Framework.ModHelpers
@ -179,16 +179,14 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists.");
// create manifest
IManifest manifest = new Manifest
{
Name = name,
Author = author,
Description = description,
Version = version,
UniqueID = id,
UpdateKeys = new string[0],
ContentPackFor = new ManifestContentPackFor { UniqueID = this.ModID }
};
IManifest manifest = new Manifest(
uniqueID: id,
name: name,
author: author,
description: description,
version: version,
contentPackFor: new ManifestContentPackFor(this.ModID)
);
// create content pack
return this.CreateContentPack(directoryPath, manifest);

View File

@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.ModData;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Utilities;
using ToolkitManifest = StardewModdingAPI.Toolkit.Serialisation.Models.Manifest;
namespace StardewModdingAPI.Framework.ModLoading
{
@ -28,25 +28,28 @@ namespace StardewModdingAPI.Framework.ModLoading
{
// read file
Manifest manifest = null;
string path = Path.Combine(modDir.FullName, "manifest.json");
string error = null;
try
{
manifest = jsonHelper.ReadJsonFile<Manifest>(path);
if (manifest == null)
string path = Path.Combine(modDir.FullName, "manifest.json");
try
{
error = File.Exists(path)
? "its manifest is invalid."
: "it doesn't have a manifest.";
ToolkitManifest rawManifest = jsonHelper.ReadJsonFile<ToolkitManifest>(path);
if (rawManifest == null)
{
error = File.Exists(path)
? "its manifest is invalid."
: "it doesn't have a manifest.";
}
manifest = new Manifest(rawManifest);
}
catch (SParseException ex)
{
error = $"parsing its manifest failed: {ex.Message}";
}
catch (Exception ex)
{
error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
}
}
catch (SParseException ex)
{
error = $"parsing its manifest failed: {ex.Message}";
}
catch (Exception ex)
{
error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
}
// parse internal data record (if any)

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using StardewModdingAPI.Framework.Serialisation.SmapiConverters;
namespace StardewModdingAPI.Framework.Models
{
@ -11,39 +11,87 @@ namespace StardewModdingAPI.Framework.Models
** Accessors
*********/
/// <summary>The mod name.</summary>
public string Name { get; set; }
public string Name { get; }
/// <summary>A brief description of the mod.</summary>
public string Description { get; set; }
public string Description { get; }
/// <summary>The mod author's name.</summary>
public string Author { get; set; }
public string Author { get; }
/// <summary>The mod version.</summary>
public ISemanticVersion Version { get; set; }
public ISemanticVersion Version { get; }
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
public ISemanticVersion MinimumApiVersion { get; set; }
public ISemanticVersion MinimumApiVersion { get; }
/// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary>
public string EntryDll { get; set; }
public string EntryDll { get; }
/// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="IManifest.EntryDll"/>.</summary>
[JsonConverter(typeof(ManifestContentPackForConverter))]
public IManifestContentPackFor ContentPackFor { get; set; }
public IManifestContentPackFor ContentPackFor { get; }
/// <summary>The other mods that must be loaded before this mod.</summary>
[JsonConverter(typeof(ManifestDependencyArrayConverter))]
public IManifestDependency[] Dependencies { get; set; }
public IManifestDependency[] Dependencies { get; }
/// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
public string[] UpdateKeys { get; set; }
/// <summary>The unique mod ID.</summary>
public string UniqueID { get; set; }
public string UniqueID { get; }
/// <summary>Any manifest fields which didn't match a valid field.</summary>
[JsonExtensionData]
public IDictionary<string, object> ExtraFields { get; set; }
public IDictionary<string, object> ExtraFields { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="manifest">The toolkit manifest.</param>
public Manifest(Toolkit.Serialisation.Models.Manifest manifest)
: this(
uniqueID: manifest.UniqueID,
name: manifest.Name,
author: manifest.Author,
description: manifest.Description,
version: manifest.Version != null ? new SemanticVersion(manifest.Version) : null,
entryDll: manifest.EntryDll,
minimumApiVersion: manifest.MinimumApiVersion != null ? new SemanticVersion(manifest.MinimumApiVersion) : null,
contentPackFor: manifest.ContentPackFor != null ? new ManifestContentPackFor(manifest.ContentPackFor) : null,
dependencies: manifest.Dependencies?.Select(p => p != null ? (IManifestDependency)new ManifestDependency(p) : null).ToArray(),
updateKeys: manifest.UpdateKeys,
extraFields: manifest.ExtraFields
)
{ }
/// <summary>Construct an instance for a transitional content pack.</summary>
/// <param name="uniqueID">The unique mod ID.</param>
/// <param name="name">The mod name.</param>
/// <param name="author">The mod author's name.</param>
/// <param name="description">A brief description of the mod.</param>
/// <param name="version">The mod version.</param>
/// <param name="entryDll">The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method. Mutually exclusive with <paramref name="contentPackFor"/>.</param>
/// <param name="minimumApiVersion">The minimum SMAPI version required by this mod, if any.</param>
/// <param name="contentPackFor">The modID which will read this as a content pack. Mutually exclusive with <paramref name="entryDll"/>.</param>
/// <param name="dependencies">The other mods that must be loaded before this mod.</param>
/// <param name="updateKeys">The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</param>
/// <param name="extraFields">Any manifest fields which didn't match a valid field.</param>
public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string entryDll = null, ISemanticVersion minimumApiVersion = null, IManifestContentPackFor contentPackFor = null, IManifestDependency[] dependencies = null, string[] updateKeys = null, IDictionary<string, object> extraFields = null)
{
this.Name = name;
this.Author = author;
this.Description = description;
this.Version = version;
this.UniqueID = uniqueID;
this.UpdateKeys = new string[0];
this.EntryDll = entryDll;
this.ContentPackFor = contentPackFor;
this.MinimumApiVersion = minimumApiVersion;
this.Dependencies = dependencies ?? new IManifestDependency[0];
this.UpdateKeys = updateKeys ?? new string[0];
this.ExtraFields = extraFields;
}
}
}

View File

@ -7,9 +7,30 @@ namespace StardewModdingAPI.Framework.Models
** Accessors
*********/
/// <summary>The unique ID of the mod which can read this content pack.</summary>
public string UniqueID { get; set; }
public string UniqueID { get; }
/// <summary>The minimum required version (if any).</summary>
public ISemanticVersion MinimumVersion { get; set; }
public ISemanticVersion MinimumVersion { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="contentPackFor">The toolkit instance.</param>
public ManifestContentPackFor(Toolkit.Serialisation.Models.ManifestContentPackFor contentPackFor)
{
this.UniqueID = contentPackFor.UniqueID;
this.MinimumVersion = new SemanticVersion(contentPackFor.MinimumVersion);
}
/// <summary>Construct an instance.</summary>
/// <param name="uniqueID">The unique ID of the mod which can read this content pack.</param>
/// <param name="minimumVersion">The minimum required version (if any).</param>
public ManifestContentPackFor(string uniqueID, ISemanticVersion minimumVersion = null)
{
this.UniqueID = uniqueID;
this.MinimumVersion = minimumVersion;
}
}
}

View File

@ -7,18 +7,23 @@ namespace StardewModdingAPI.Framework.Models
** Accessors
*********/
/// <summary>The unique mod ID to require.</summary>
public string UniqueID { get; set; }
public string UniqueID { get; }
/// <summary>The minimum required version (if any).</summary>
public ISemanticVersion MinimumVersion { get; set; }
public ISemanticVersion MinimumVersion { get; }
/// <summary>Whether the dependency must be installed to use the mod.</summary>
public bool IsRequired { get; set; }
public bool IsRequired { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="dependency">The toolkit instance.</param>
public ManifestDependency(Toolkit.Serialisation.Models.ManifestDependency dependency)
: this(dependency.UniqueID, dependency.MinimumVersion?.ToString(), dependency.IsRequired) { }
/// <summary>Construct an instance.</summary>
/// <param name="uniqueID">The unique mod ID to require.</param>
/// <param name="minimumVersion">The minimum required version (if any).</param>

View File

@ -1,9 +1,10 @@
using System;
using Microsoft.Xna.Framework;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters
namespace StardewModdingAPI.Framework.Serialisation
{
/// <summary>Handles deserialisation of <see cref="Color"/> for crossplatform compatibility.</summary>
/// <remarks>

View File

@ -1,9 +1,10 @@
using System;
using Microsoft.Xna.Framework;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters
namespace StardewModdingAPI.Framework.Serialisation
{
/// <summary>Handles deserialisation of <see cref="PointConverter"/> for crossplatform compatibility.</summary>
/// <remarks>

View File

@ -2,9 +2,10 @@ using System;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters
namespace StardewModdingAPI.Framework.Serialisation
{
/// <summary>Handles deserialisation of <see cref="Rectangle"/> for crossplatform compatibility.</summary>
/// <remarks>

View File

@ -36,7 +36,7 @@ namespace StardewModdingAPI
IManifestDependency[] Dependencies { get; }
/// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
string[] UpdateKeys { get; set; }
string[] UpdateKeys { get; }
/// <summary>Any manifest fields which didn't match a valid field.</summary>
IDictionary<string, object> ExtraFields { get; }

View File

@ -10,6 +10,7 @@ using System.Runtime.ExceptionServices;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.Xna.Framework.Input;
#if SMAPI_FOR_WINDOWS
using System.Windows.Forms;
#endif
@ -27,8 +28,11 @@ using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Serialisation;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using Keys = System.Windows.Forms.Keys;
using Monitor = StardewModdingAPI.Framework.Monitor;
using SObject = StardewValley.Object;
using ThreadState = System.Threading.ThreadState;
@ -148,6 +152,18 @@ namespace StardewModdingAPI
};
this.EventManager = new EventManager(this.Monitor, this.ModRegistry);
// init JSON parser
JsonConverter[] converters = {
new StringEnumConverter<Buttons>(),
new StringEnumConverter<Keys>(),
new StringEnumConverter<SButton>(),
new ColorConverter(),
new PointConverter(),
new RectangleConverter()
};
foreach (JsonConverter converter in converters)
this.JsonHelper.JsonSettings.Converters.Add(converter);
// hook up events
ContentEvents.Init(this.EventManager);
ControlEvents.Init(this.EventManager);
@ -1093,7 +1109,7 @@ namespace StardewModdingAPI
/// <param name="mods">The mods for which to reload translations.</param>
private void ReloadTranslations(IEnumerable<IModMetadata> mods)
{
JsonHelper jsonHelper = new JsonHelper();
JsonHelper jsonHelper = this.JsonHelper;
foreach (IModMetadata metadata in mods)
{
if (metadata.IsContentPack)

View File

@ -103,6 +103,9 @@
<Compile Include="Framework\ContentManagers\GameContentManager.cs" />
<Compile Include="Framework\ContentManagers\IContentManager.cs" />
<Compile Include="Framework\ContentManagers\ModContentManager.cs" />
<Compile Include="Framework\Serialisation\ColorConverter.cs" />
<Compile Include="Framework\Serialisation\PointConverter.cs" />
<Compile Include="Framework\Serialisation\RectangleConverter.cs" />
<Compile Include="Framework\Events\ModEventsBase.cs" />
<Compile Include="Framework\Events\EventManager.cs" />
<Compile Include="Events\IModEvents.cs" />
@ -114,16 +117,17 @@
<Compile Include="Framework\Events\ModEvents.cs" />
<Compile Include="Framework\Events\ModInputEvents.cs" />
<Compile Include="Framework\Input\GamePadStateBuilder.cs" />
<Compile Include="Framework\Models\Manifest.cs" />
<Compile Include="Framework\Models\ManifestContentPackFor.cs" />
<Compile Include="Framework\Models\ManifestDependency.cs" />
<Compile Include="Framework\ModHelpers\InputHelper.cs" />
<Compile Include="IInputHelper.cs" />
<Compile Include="Framework\Input\SInputState.cs" />
<Compile Include="Framework\Input\InputStatus.cs" />
<Compile Include="Framework\LegacyManifestVersion.cs" />
<Compile Include="Framework\ModData\ModDatabase.cs" />
<Compile Include="Framework\ModData\ModDataField.cs" />
<Compile Include="Framework\ModData\ModDataFieldKey.cs" />
<Compile Include="Framework\ModData\ParsedModDataRecord.cs" />
<Compile Include="Framework\Models\ManifestContentPackFor.cs" />
<Compile Include="Framework\Models\SMetadata.cs" />
<Compile Include="Framework\ModHelpers\MultiplayerHelper.cs" />
<Compile Include="Framework\ModLoading\Finders\EventFinder.cs" />
@ -151,13 +155,6 @@
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyFactory.cs" />
<Compile Include="Framework\RewriteFacades\SpriteBatchMethods.cs" />
<Compile Include="Framework\Serialisation\SmapiConverters\ManifestContentPackForConverter.cs" />
<Compile Include="Framework\Serialisation\SmapiConverters\ManifestDependencyArrayConverter.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\SMultiplayer.cs" />
<Compile Include="Framework\StateTracking\Comparers\EquatableComparer.cs" />
<Compile Include="Framework\StateTracking\Comparers\ObjectReferenceComparer.cs" />
@ -235,16 +232,12 @@
<Compile Include="Context.cs" />
<Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" />
<Compile Include="Framework\Logging\InterceptingTextWriter.cs" />
<Compile Include="Framework\Models\ManifestDependency.cs" />
<Compile Include="Framework\ModData\ModStatus.cs" />
<Compile Include="Framework\Models\SConfig.cs" />
<Compile Include="Framework\ModLoading\ModMetadata.cs" />
<Compile Include="Framework\Reflection\ReflectedProperty.cs" />
<Compile Include="Framework\RequestExitDelegate.cs" />
<Compile Include="Framework\ContentCoordinator.cs" />
<Compile Include="Framework\Exceptions\SParseException.cs" />
<Compile Include="Framework\Serialisation\JsonHelper.cs" />
<Compile Include="Framework\Serialisation\SmapiConverters\StringEnumConverter.cs" />
<Compile Include="IAssetEditor.cs" />
<Compile Include="IAssetInfo.cs" />
<Compile Include="IAssetLoader.cs" />
@ -283,7 +276,6 @@
<Compile Include="Events\ChangeType.cs" />
<Compile Include="Events\ItemStackChange.cs" />
<Compile Include="Framework\Monitor.cs" />
<Compile Include="Framework\Models\Manifest.cs" />
<Compile Include="Metadata\InstructionMetadata.cs" />
<Compile Include="Mod.cs" />
<Compile Include="PatchMode.cs" />

View File

@ -1,11 +1,11 @@
using System;
using Newtonsoft.Json;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
namespace StardewModdingAPI.Toolkit.Serialisation.Converters
{
/// <summary>Handles deserialisation of <see cref="IManifestContentPackFor"/> arrays.</summary>
internal class ManifestContentPackForConverter : JsonConverter
/// <summary>Handles deserialisation of <see cref="ManifestContentPackFor"/> arrays.</summary>
public class ManifestContentPackForConverter : JsonConverter
{
/*********
** Accessors
@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
/// <param name="objectType">The object type.</param>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IManifestContentPackFor[]);
return objectType == typeof(ManifestContentPackFor[]);
}

View File

@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
namespace StardewModdingAPI.Toolkit.Serialisation.Converters
{
/// <summary>Handles deserialisation of <see cref="IManifestDependency"/> arrays.</summary>
/// <summary>Handles deserialisation of <see cref="ManifestDependency"/> arrays.</summary>
internal class ManifestDependencyArrayConverter : JsonConverter
{
/*********
@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
/// <param name="objectType">The object type.</param>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IManifestDependency[]);
return objectType == typeof(ManifestDependency[]);
}
@ -37,12 +37,12 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
/// <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>();
List<ManifestDependency> result = new List<ManifestDependency>();
foreach (JObject obj in JArray.Load(reader).Children<JObject>())
{
string uniqueID = obj.ValueIgnoreCase<string>(nameof(IManifestDependency.UniqueID));
string minVersion = obj.ValueIgnoreCase<string>(nameof(IManifestDependency.MinimumVersion));
bool required = obj.ValueIgnoreCase<bool?>(nameof(IManifestDependency.IsRequired)) ?? true;
string uniqueID = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.UniqueID));
string minVersion = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.MinimumVersion));
bool required = obj.ValueIgnoreCase<bool?>(nameof(ManifestDependency.IsRequired)) ?? true;
result.Add(new ManifestDependency(uniqueID, minVersion, required));
}
return result.ToArray();

View File

@ -1,10 +1,10 @@
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
namespace StardewModdingAPI.Toolkit.Serialisation.Converters
{
/// <summary>Handles deserialisation of <see cref="SemanticVersion"/>.</summary>
internal class SemanticVersionConverter : SimpleReadOnlyConverter<ISemanticVersion>
internal class SemanticVersionConverter : SimpleReadOnlyConverter<SemanticVersion>
{
/*********
** Protected methods
@ -12,25 +12,25 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
/// <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)
protected override SemanticVersion ReadObject(JObject obj, string path)
{
int major = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MajorVersion));
int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion));
int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion));
string build = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.Build));
int major = obj.ValueIgnoreCase<int>("MajorVersion");
int minor = obj.ValueIgnoreCase<int>("MinorVersion");
int patch = obj.ValueIgnoreCase<int>("PatchVersion");
string build = obj.ValueIgnoreCase<string>("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)
protected override SemanticVersion 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;
return (SemanticVersion)version;
}
}
}

View File

@ -1,9 +1,8 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Framework.Exceptions;
namespace StardewModdingAPI.Framework.Serialisation
namespace StardewModdingAPI.Toolkit.Serialisation.Converters
{
/// <summary>The base implementation for simplified converters which deserialise <typeparamref name="T"/> without overriding serialisation.</summary>
/// <typeparam name="T">The type to deserialise.</typeparam>

View File

@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json.Converters;
namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters
namespace StardewModdingAPI.Toolkit.Serialisation.Converters
{
/// <summary>A variant of <see cref="StringEnumConverter"/> which only converts a specified enum.</summary>
/// <typeparam name="T">The enum type.</typeparam>

View File

@ -0,0 +1,21 @@
using System;
using Newtonsoft.Json.Linq;
namespace StardewModdingAPI.Toolkit.Serialisation
{
/// <summary>Provides extension methods for parsing JSON.</summary>
public static class JsonExtensions
{
/// <summary>Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="obj">The JSON object to search.</param>
/// <param name="fieldName">The field name.</param>
public static T ValueIgnoreCase<T>(this JObject obj, string fieldName)
{
JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase);
return token != null
? token.Value<T>()
: default(T);
}
}
}

View File

@ -1,39 +1,23 @@
using System;
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;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Framework.Serialisation
namespace StardewModdingAPI.Toolkit.Serialisation
{
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
internal class JsonHelper
public class JsonHelper
{
/*********
** Accessors
*********/
/// <summary>The JSON settings to use when serialising and deserialising files.</summary>
private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
Converters = new List<JsonConverter>
{
// SMAPI types
new SemanticVersionConverter(),
// enums
new StringEnumConverter<Buttons>(),
new StringEnumConverter<Keys>(),
new StringEnumConverter<SButton>(),
// crossplatform compatibility
new ColorConverter(),
new PointConverter(),
new RectangleConverter()
}
Converters = new List<JsonConverter> { new SemanticVersionConverter() }
};

View File

@ -1,9 +1,9 @@
using Newtonsoft.Json;
namespace StardewModdingAPI.Framework
namespace StardewModdingAPI.Toolkit.Serialisation.Models
{
/// <summary>An implementation of <see cref="ISemanticVersion"/> that hamdles the legacy <see cref="IManifest"/> version format.</summary>
internal class LegacyManifestVersion : SemanticVersion
/// <summary>An implementation of <see cref="ISemanticVersion"/> that hamdles the legacy <see cref="Manifest"/> version format.</summary>
public class LegacyManifestVersion : SemanticVersion
{
/*********
** Public methods

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Toolkit.Serialisation.Models
{
/// <summary>A manifest which describes a mod for SMAPI.</summary>
public class Manifest
{
/*********
** Accessors
*********/
/// <summary>The mod name.</summary>
public string Name { get; set; }
/// <summary>A brief description of the mod.</summary>
public string Description { get; set; }
/// <summary>The mod author's name.</summary>
public string Author { get; set; }
/// <summary>The mod version.</summary>
public SemanticVersion Version { get; set; }
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
public SemanticVersion MinimumApiVersion { get; set; }
/// <summary>The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary>
public string EntryDll { get; set; }
/// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="Manifest.EntryDll"/>.</summary>
[JsonConverter(typeof(ManifestContentPackForConverter))]
public ManifestContentPackFor ContentPackFor { get; set; }
/// <summary>The other mods that must be loaded before this mod.</summary>
[JsonConverter(typeof(ManifestDependencyArrayConverter))]
public ManifestDependency[] Dependencies { get; set; }
/// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
public string[] UpdateKeys { get; set; }
/// <summary>The unique mod ID.</summary>
public string UniqueID { get; set; }
/// <summary>Any manifest fields which didn't match a valid field.</summary>
[JsonExtensionData]
public IDictionary<string, object> ExtraFields { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace StardewModdingAPI.Toolkit.Serialisation.Models
{
/// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary>
public class ManifestContentPackFor
{
/*********
** Accessors
*********/
/// <summary>The unique ID of the mod which can read this content pack.</summary>
public string UniqueID { get; set; }
/// <summary>The minimum required version (if any).</summary>
public SemanticVersion MinimumVersion { get; set; }
}
}

View File

@ -0,0 +1,35 @@
namespace StardewModdingAPI.Toolkit.Serialisation.Models
{
/// <summary>A mod dependency listed in a mod manifest.</summary>
public class ManifestDependency
{
/*********
** Accessors
*********/
/// <summary>The unique mod ID to require.</summary>
public string UniqueID { get; set; }
/// <summary>The minimum required version (if any).</summary>
public SemanticVersion MinimumVersion { get; set; }
/// <summary>Whether the dependency must be installed to use the mod.</summary>
public bool IsRequired { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="uniqueID">The unique mod ID to require.</param>
/// <param name="minimumVersion">The minimum required version (if any).</param>
/// <param name="required">Whether the dependency must be installed to use the mod.</param>
public ManifestDependency(string uniqueID, string minimumVersion, bool required = true)
{
this.UniqueID = uniqueID;
this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion)
? new SemanticVersion(minimumVersion)
: null;
this.IsRequired = required;
}
}
}

View File

@ -1,6 +1,6 @@
using System;
using System;
namespace StardewModdingAPI.Framework.Exceptions
namespace StardewModdingAPI.Toolkit.Serialisation
{
/// <summary>A format exception which provides a user-facing error message.</summary>
internal class SParseException : FormatException