diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-16 18:50:46 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-16 18:50:46 -0500 |
commit | 024489c33827ce8e1463eac199daa996a8a99216 (patch) | |
tree | c0a314f1bf1476656153d929104682faba86577a /src/SMAPI/Framework | |
parent | cf383870837748e83b99bf63d36d7a8709743715 (diff) | |
download | SMAPI-024489c33827ce8e1463eac199daa996a8a99216.tar.gz SMAPI-024489c33827ce8e1463eac199daa996a8a99216.tar.bz2 SMAPI-024489c33827ce8e1463eac199daa996a8a99216.zip |
overhaul internal mod data format (#439)
The new format is much more concise, reduces the memory footprint by only parsing metadata for loaded mods, and adds support for versioning and defaulting most fields.
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r-- | src/SMAPI/Framework/IModMetadata.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadata.cs | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModResolver.cs | 24 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModCompatibility.cs | 55 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModDataField.cs | 82 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModDataFieldKey.cs | 18 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModDataID.cs | 85 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ModDataRecord.cs | 220 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/ParsedModDataRecord.cs | 48 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs | 61 | ||||
-rw-r--r-- | src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs | 19 |
11 files changed, 361 insertions, 257 deletions
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index a36994fd..41484567 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework IManifest Manifest { get; } /// <summary>>Metadata about the mod from SMAPI's internal data (if any).</summary> - ModDataRecord DataRecord { get; } + ParsedModDataRecord DataRecord { get; } /// <summary>The metadata resolution status.</summary> ModMetadataStatus Status { get; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 30fe211b..1a71920e 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary> - public ModDataRecord DataRecord { get; } + public ParsedModDataRecord DataRecord { get; } /// <summary>The metadata resolution status.</summary> public ModMetadataStatus Status { get; private set; } @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="directoryPath">The mod's full directory path.</param> /// <param name="manifest">The mod manifest.</param> /// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param> - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9802d9e9..6671e880 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -53,18 +53,15 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get internal data record (if any) - ModDataRecord dataRecord = null; + // parse internal data record (if any) + ParsedModDataRecord dataRecord = null; if (manifest != null) { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); + ModDataRecord rawDataRecord = dataRecords.FirstOrDefault(p => p.Matches(manifest)); + if (rawDataRecord != null) + dataRecord = rawDataRecord.ParseFieldsFor(manifest); } - // add default update keys - if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) - manifest.UpdateKeys = dataRecord.UpdateKeys; - // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name @@ -93,17 +90,16 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // validate compatibility - ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); - switch (compatibility?.Status) + switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List<string> updateUrls = new List<string>(); @@ -124,10 +120,10 @@ namespace StardewModdingAPI.Framework.ModLoading // build error string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) error += "newer version"; else - error += $"version newer than {compatibility.UpperVersion}"; + error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); diff --git a/src/SMAPI/Framework/Models/ModCompatibility.cs b/src/SMAPI/Framework/Models/ModCompatibility.cs deleted file mode 100644 index 54737e6c..00000000 --- a/src/SMAPI/Framework/Models/ModCompatibility.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Specifies the compatibility of a given mod version range.</summary> - internal class ModCompatibility - { - /********* - ** Accessors - *********/ - /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> - public ISemanticVersion LowerVersion { get; } - - /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> - public ISemanticVersion UpperVersion { get; } - - /// <summary>The mod compatibility.</summary> - public ModStatus Status { get; } - - /// <summary>The reason phrase to show in log output, or <c>null</c> to use the default value.</summary> - /// <example>For example, "this version is incompatible with the latest version of the game".</example> - public string ReasonPhrase { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="versionRange">A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range.</param> - /// <param name="status">The mod compatibility.</param> - /// <param name="reasonPhrase">The reason phrase to show in log output, or <c>null</c> to use the default value.</param> - public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) - { - // extract version strings - string[] versions = versionRange.Split('~'); - if (versions.Length != 2) - throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); - - // initialise - this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; - this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; - this.Status = status; - this.ReasonPhrase = reasonPhrase; - } - - /// <summary>Get whether a given version is contained within this compatibility range.</summary> - /// <param name="version">The version to check.</param> - public bool MatchesVersion(ISemanticVersion version) - { - return - (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataField.cs b/src/SMAPI/Framework/Models/ModDataField.cs new file mode 100644 index 00000000..0812b39b --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>A versioned mod metadata field.</summary> + internal class ModDataField + { + /********* + ** Accessors + *********/ + /// <summary>The field key.</summary> + public ModDataFieldKey Key { get; } + + /// <summary>The field value.</summary> + public string Value { get; } + + /// <summary>Whether this field should only be applied if it's not already set.</summary> + public bool IsDefault { get; } + + /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> + public ISemanticVersion LowerVersion { get; } + + /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="key">The field key.</param> + /// <param name="value">The field value.</param> + /// <param name="isDefault">Whether this field should only be applied if it's not already set.</param> + /// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param> + /// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param> + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// <summary>Get whether this data field applies for the given manifest.</summary> + /// <param name="manifest">The mod manifest.</param> + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get whether a manifest field has a meaningful value for the purposes of enforcing <see cref="IsDefault"/>.</summary> + /// <param name="manifest">The mod manifest.</param> + /// <param name="key">The field key matching <see cref="ModDataFieldKey"/>.</param> + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI/Framework/Models/ModDataFieldKey.cs b/src/SMAPI/Framework/Models/ModDataFieldKey.cs new file mode 100644 index 00000000..5767afc9 --- /dev/null +++ b/src/SMAPI/Framework/Models/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>The valid field keys.</summary> + public enum ModDataFieldKey + { + /// <summary>A manifest update key.</summary> + UpdateKey, + + /// <summary>An alternative URL the player can check for an updated version.</summary> + AlternativeUrl, + + /// <summary>The mod's predefined compatibility status.</summary> + Status, + + /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary> + StatusReasonPhrase + } +} diff --git a/src/SMAPI/Framework/Models/ModDataID.cs b/src/SMAPI/Framework/Models/ModDataID.cs deleted file mode 100644 index d19434fa..00000000 --- a/src/SMAPI/Framework/Models/ModDataID.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Uniquely identifies a mod in SMAPI's internal data.</summary> - /// <remarks> - /// This represents a custom format which uniquely identifies a mod across all versions, even - /// if its field values change or it doesn't specify a unique ID. This is mapped to a string - /// with the following format: - /// - /// 1. If the mod's identifier changed over time, multiple variants can be separated by the <c>|</c> - /// character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. - /// </remarks> - internal class ModDataID - { - /********* - ** Properties - *********/ - /// <summary>The unique sets of field values which identify this mod.</summary> - private readonly FieldSnapshot[] Snapshots; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public ModDataID() { } - - /// <summary>Construct an instance.</summary> - /// <param name="data">The mod identifier string (see remarks on <see cref="ModDataID"/>).</param> - public ModDataID(string data) - { - this.Snapshots = - ( - from string part in data.Split('|') - let str = part.Trim() - select str.StartsWith("{") - ? JsonConvert.DeserializeObject<FieldSnapshot>(str) - : new FieldSnapshot { ID = str } - ) - .ToArray(); - } - - /// <summary>Get whether this ID matches a given mod manifest.</summary> - /// <param name="id">The mod's unique ID, or a substitute ID if it isn't set in the manifest.</param> - /// <param name="manifest">The manifest to check.</param> - public bool Matches(string id, IManifest manifest) - { - return this.Snapshots.Any(snapshot => - snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) - && ( - snapshot.Author == null - || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) - ); - } - - - /********* - ** Private models - *********/ - /// <summary>A unique set of fields which identifies the mod.</summary> - private class FieldSnapshot - { - /********* - ** Accessors - *********/ - /// <summary>The unique mod ID.</summary> - public string ID { get; set; } - - /// <summary>The mod name, or <c>null</c> to ignore the mod name.</summary> - public string Name { get; set; } - - /// <summary>The author name, or <c>null</c> to ignore the author.</summary> - public string Author { get; set; } - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs index 580acb70..2c26741c 100644 --- a/src/SMAPI/Framework/Models/ModDataRecord.cs +++ b/src/SMAPI/Framework/Models/ModDataRecord.cs @@ -1,49 +1,188 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.Serialization; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; +using Newtonsoft.Json.Linq; namespace StardewModdingAPI.Framework.Models { - /// <summary>Metadata about a mod from SMAPI's internal data.</summary> + /// <summary>Raw mod metadata from SMAPI's internal mod list.</summary> internal class ModDataRecord { /********* - ** Accessors + ** Properties *********/ - /// <summary>The unique mod identifier.</summary> - [JsonConverter(typeof(ModDataIdConverter))] - public ModDataID ID { get; set; } + /// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary> + [JsonExtensionData] + private IDictionary<string, JToken> ExtensionData; - /// <summary>A value to inject into <see cref="IManifest.UpdateKeys"/> field if it's not already set.</summary> - public string[] UpdateKeys { get; set; } - /// <summary>The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible.</summary> - public string AlternativeUrl { get; set; } + /********* + ** Accessors + *********/ + /// <summary>The mod's current unique ID.</summary> + public string ID { get; set; } - /// <summary>The compatibility of given mod versions (if any).</summary> - [JsonConverter(typeof(ModCompatibilityArrayConverter))] - public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; + /// <summary>The former mod IDs (if any).</summary> + /// <remarks> + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// <c>|</c> character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// </remarks> + public string FormerIDs { get; set; } - /// <summary>Map local versions to a semantic version for update checks.</summary> + /// <summary>Maps local versions to a semantic version for update checks.</summary> public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>(); - /// <summary>Map remote versions to a semantic version for update checks.</summary> + /// <summary>Maps remote versions to a semantic version for update checks.</summary> public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>(); + /// <summary>The versioned field data.</summary> + /// <remarks> + /// This maps field names to values. This should be accessed via <see cref="GetFields"/>. + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and <c>Default</c>, separated by pipes (whitespace trimmed). For example, <c>Name</c> + /// will always override the name, <c>Default | Name</c> will only override a blank + /// name, and <c>~1.1 | Default | Name</c> will override blank names up to version 1.1. + /// - The version format is <c>min~max</c> (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a <see cref="ModDataFieldKey"/> value. + /// </remarks> + public IDictionary<string, string> Fields { get; set; } = new Dictionary<string, string>(); + /********* ** Public methods *********/ - /// <summary>Get the compatibility record for a given version, if any.</summary> - /// <param name="version">The mod version to check.</param> - public ModCompatibility GetCompatibility(ISemanticVersion version) + /// <summary>Get whether the manifest matches the <see cref="FormerIDs"/> field.</summary> + /// <param name="manifest">The mod manifest to check.</param> + public bool Matches(IManifest manifest) + { + // try main ID + if (this.ID != null && this.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (this.FormerIDs != null) + { + foreach (string part in this.FormerIDs.Split('|')) + { + // packed field snapshot + if (part.StartsWith("{")) + { + FieldSnapshot snapshot = JsonConvert.DeserializeObject<FieldSnapshot>(part); + bool isMatch = + (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase)) + && ( + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + + if (isMatch) + return true; + } + + // plain ID + else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + + // no match + return false; + } + + /// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary> + public IEnumerable<ModDataField> GetFields() { - return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); + foreach (KeyValuePair<string, string> pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// <summary>Get a parsed representation of the <see cref="Fields"/> which match a given manifest.</summary> + /// <param name="manifest">The manifest to match.</param> + public ParsedModDataRecord ParseFieldsFor(IManifest manifest) + { + ParsedModDataRecord parsed = new ParsedModDataRecord { DataRecord = this }; + foreach (ModDataField field in this.GetFields().Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; } /// <summary>Get a semantic local version for update checks.</summary> - /// <param name="version">The local version to normalise.</param> + /// <param name="version">The remote version to normalise.</param> public string GetLocalVersionForUpdateChecks(string version) { return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) @@ -59,5 +198,46 @@ namespace StardewModdingAPI.Framework.Models ? newVersion : version; } + + + /********* + ** Private methods + *********/ + /// <summary>The method invoked after JSON deserialisation.</summary> + /// <param name="context">The deserialisation context.</param> + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + + + /********* + ** Private models + *********/ + /// <summary>A unique set of fields which identifies the mod.</summary> + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod ID (or <c>null</c> to ignore it).</summary> + public string ID { get; set; } + + /// <summary>The entry DLL (or <c>null</c> to ignore it).</summary> + public string EntryDll { get; set; } + + /// <summary>The mod name (or <c>null</c> to ignore it).</summary> + public string Name { get; set; } + + /// <summary>The author name (or <c>null</c> to ignore it).</summary> + public string Author { get; set; } + } } } diff --git a/src/SMAPI/Framework/Models/ParsedModDataRecord.cs b/src/SMAPI/Framework/Models/ParsedModDataRecord.cs new file mode 100644 index 00000000..0abc7b89 --- /dev/null +++ b/src/SMAPI/Framework/Models/ParsedModDataRecord.cs @@ -0,0 +1,48 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>A parsed representation of the fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary> + internal class ParsedModDataRecord + { + /********* + ** Accessors + *********/ + /// <summary>The underlying data record.</summary> + public ModDataRecord DataRecord { get; set; } + + /// <summary>The update key to apply.</summary> + public string UpdateKey { get; set; } + + /// <summary>The mod version to apply.</summary> + public ISemanticVersion Version { get; set; } + + /// <summary>The alternative URL the player can check for an updated version.</summary> + public string AlternativeUrl { get; set; } + + /// <summary>The predefined compatibility status.</summary> + public ModStatus Status { get; set; } = ModStatus.None; + + /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary> + public string StatusReasonPhrase { get; set; } + + /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Get a semantic local version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetLocalVersionForUpdateChecks(string version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// <summary>Get a semantic remote version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + } + } +} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs deleted file mode 100644 index 3232dde4..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// <summary>Handles deserialisation of <see cref="ModCompatibility"/> arrays.</summary> - internal class ModCompatibilityArrayConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// <summary>Whether this converter can write JSON.</summary> - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// <summary>Get whether this instance can convert the specified object type.</summary> - /// <param name="objectType">The object type.</param> - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ModCompatibility[]); - } - - - /********* - ** Protected methods - *********/ - /// <summary>Read the JSON representation of the object.</summary> - /// <param name="reader">The JSON reader.</param> - /// <param name="objectType">The object type.</param> - /// <param name="existingValue">The object being read.</param> - /// <param name="serializer">The calling serializer.</param> - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List<ModCompatibility> result = new List<ModCompatibility>(); - foreach (JProperty property in JObject.Load(reader).Properties()) - { - string range = property.Name; - ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value<string>(nameof(ModCompatibility.Status))); - string reasonPhrase = property.Value.Value<string>(nameof(ModCompatibility.ReasonPhrase)); - - result.Add(new ModCompatibility(range, status, reasonPhrase)); - } - return result.ToArray(); - } - - /// <summary>Writes the JSON representation of the object.</summary> - /// <param name="writer">The JSON writer.</param> - /// <param name="value">The value.</param> - /// <param name="serializer">The calling serializer.</param> - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs deleted file mode 100644 index 8a10db47..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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); - } - } -} |