summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI.Toolkit/Framework/ModData
diff options
context:
space:
mode:
Diffstat (limited to 'src/StardewModdingAPI.Toolkit/Framework/ModData')
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs82
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs18
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs146
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs140
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs18
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs51
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs14
7 files changed, 469 insertions, 0 deletions
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs
new file mode 100644
index 00000000..b3954693
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs
@@ -0,0 +1,82 @@
+using System.Linq;
+
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <summary>A versioned mod metadata field.</summary>
+ public 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(p => !string.IsNullOrWhiteSpace(p));
+
+ // non-manifest fields
+ case ModDataFieldKey.AlternativeUrl:
+ case ModDataFieldKey.StatusReasonPhrase:
+ case ModDataFieldKey.Status:
+ return false;
+
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs
new file mode 100644
index 00000000..09dd0cc5
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <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/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs
new file mode 100644
index 00000000..97ad0ca4
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <summary>Raw mod metadata from SMAPI's internal mod list.</summary>
+ public class ModDataRecord
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <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;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's current unique ID.</summary>
+ public string ID { get; set; }
+
+ /// <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>Maps local versions to a semantic version for update checks.</summary>
+ public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>();
+
+ /// <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 a parsed representation of the <see cref="Fields"/>.</summary>
+ public IEnumerable<ModDataField> GetFields()
+ {
+ 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 semantic local version for update checks.</summary>
+ /// <param name="version">The remote version to normalise.</param>
+ public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
+ {
+ return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion)
+ ? new SemanticVersion(newVersion)
+ : 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)
+ {
+ // normalise version if possible
+ if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
+ version = parsed.ToString();
+
+ // fetch remote version
+ return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion)
+ ? 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;
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs
new file mode 100644
index 00000000..c60d2bcb
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <summary>Handles access to SMAPI's internal mod metadata list.</summary>
+ public class ModDatabase
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The underlying mod data records indexed by default display name.</summary>
+ private readonly IDictionary<string, ModDataRecord> Records;
+
+ /// <summary>Get an update URL for an update key (if valid).</summary>
+ private readonly Func<string, string> GetUpdateUrl;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an empty instance.</summary>
+ public ModDatabase()
+ : this(new Dictionary<string, ModDataRecord>(), key => null) { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="records">The underlying mod data records indexed by default display name.</param>
+ /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
+ public ModDatabase(IDictionary<string, ModDataRecord> records, Func<string, string> getUpdateUrl)
+ {
+ this.Records = records;
+ this.GetUpdateUrl = getUpdateUrl;
+ }
+
+ /// <summary>Get a parsed representation of the <see cref="ModDataRecord.Fields"/> which match a given manifest.</summary>
+ /// <param name="manifest">The manifest to match.</param>
+ public ParsedModDataRecord GetParsed(IManifest manifest)
+ {
+ // get raw record
+ if (!this.TryGetRaw(manifest?.UniqueID, out string displayName, out ModDataRecord record))
+ return null;
+
+ // parse fields
+ ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record };
+ foreach (ModDataField field in record.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 the display name for a given mod ID (if available).</summary>
+ /// <param name="id">The unique mod ID.</param>
+ public string GetDisplayNameFor(string id)
+ {
+ return this.TryGetRaw(id, out string displayName, out ModDataRecord _)
+ ? displayName
+ : null;
+ }
+
+ /// <summary>Get the mod page URL for a mod (if available).</summary>
+ /// <param name="id">The unique mod ID.</param>
+ public string GetModPageUrlFor(string id)
+ {
+ // get raw record
+ if (!this.TryGetRaw(id, out string _, out ModDataRecord record))
+ return null;
+
+ // get update key
+ ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey);
+ if (updateKeyField == null)
+ return null;
+
+ // get update URL
+ return this.GetUpdateUrl(updateKeyField.Value);
+ }
+
+
+ /*********
+ ** Private models
+ *********/
+ /// <summary>Get a raw data record.</summary>
+ /// <param name="id">The mod ID to match.</param>
+ /// <param name="displayName">The mod's default display name.</param>
+ /// <param name="record">The raw mod record.</param>
+ private bool TryGetRaw(string id, out string displayName, out ModDataRecord record)
+ {
+ if (!string.IsNullOrWhiteSpace(id))
+ {
+ foreach (var entry in this.Records)
+ {
+ displayName = entry.Key;
+ record = entry.Value;
+
+ // try main ID
+ if (record.ID != null && record.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase))
+ return true;
+
+ // try former IDs
+ if (record.FormerIDs != null)
+ {
+ foreach (string part in record.FormerIDs.Split('|'))
+ {
+ if (part.Trim().Equals(id, StringComparison.InvariantCultureIgnoreCase))
+ return true;
+ }
+ }
+ }
+ }
+
+ displayName = null;
+ record = null;
+ return false;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs
new file mode 100644
index 00000000..09da74bf
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <summary>Indicates how SMAPI should treat a mod.</summary>
+ public enum ModStatus
+ {
+ /// <summary>Don't override the status.</summary>
+ None,
+
+ /// <summary>The mod is obsolete and shouldn't be used, regardless of version.</summary>
+ Obsolete,
+
+ /// <summary>Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code.</summary>
+ AssumeBroken,
+
+ /// <summary>Assume the mod is compatible, even if SMAPI detects incompatible code.</summary>
+ AssumeCompatible
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs
new file mode 100644
index 00000000..74f11ea5
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/ParsedModDataRecord.cs
@@ -0,0 +1,51 @@
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <summary>A parsed representation of the fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary>
+ public class ParsedModDataRecord
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The underlying data record.</summary>
+ public ModDataRecord DataRecord { get; set; }
+
+ /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary>
+ public string DisplayName { get; set; }
+
+ /// <summary>The update key to apply.</summary>
+ public string UpdateKey { 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 ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion 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 ISemanticVersion GetRemoteVersionForUpdateChecks(string version)
+ {
+ string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version);
+ return rawVersion != null
+ ? new SemanticVersion(rawVersion)
+ : null;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs
new file mode 100644
index 00000000..9553cca9
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModData/SMetadata.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace StardewModdingAPI.Toolkit.Framework.ModData
+{
+ /// <summary>The SMAPI predefined metadata.</summary>
+ internal class SMetadata
+ {
+ /********
+ ** Accessors
+ ********/
+ /// <summary>Extra metadata about mods.</summary>
+ public IDictionary<string, ModDataRecord> ModData { get; set; }
+ }
+}