using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Newtonsoft.Json; namespace StardewModdingAPI.Framework.ModData { /// <summary>Handles access to SMAPI's internal mod metadata list.</summary> internal 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, 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) { foreach (var entry in this.Records) { if (entry.Value.ID != null && entry.Value.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) { displayName = entry.Key; record = entry.Value; return true; } } displayName = null; record = null; return false; } /// <summary>Get a raw data record.</summary> /// <param name="manifest">The mod manifest whose fields to match.</param> /// <param name="displayName">The mod's default display name.</param> /// <param name="record">The raw mod record.</param> private bool TryGetRaw(IManifest manifest, out string displayName, out ModDataRecord record) { if (manifest != null) { foreach (var entry in this.Records) { displayName = entry.Key; record = entry.Value; // try main ID if (record.ID != null && record.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) return true; // try former IDs if (record.FormerIDs != null) { foreach (string part in record.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; } } } } displayName = null; record = null; return false; } /********* ** 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; } } } }