using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Newtonsoft.Json; namespace StardewModdingAPI.Framework.ModData { /// Handles access to SMAPI's internal mod metadata list. internal class ModDatabase { /********* ** Properties *********/ /// The underlying mod data records indexed by default display name. private readonly IDictionary Records; /// Get an update URL for an update key (if valid). private readonly Func GetUpdateUrl; /********* ** Public methods *********/ /// Construct an empty instance. public ModDatabase() : this(new Dictionary(), key => null) { } /// Construct an instance. /// The underlying mod data records indexed by default display name. /// Get an update URL for an update key (if valid). public ModDatabase(IDictionary records, Func getUpdateUrl) { this.Records = records; this.GetUpdateUrl = getUpdateUrl; } /// Get a parsed representation of the which match a given manifest. /// The manifest to match. 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; } /// Get the display name for a given mod ID (if available). /// The unique mod ID. public string GetDisplayNameFor(string id) { return this.TryGetRaw(id, out string displayName, out ModDataRecord _) ? displayName : null; } /// Get the mod page URL for a mod (if available). /// The unique mod ID. 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 *********/ /// Get a raw data record. /// The mod ID to match. /// The mod's default display name. /// The raw mod record. 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; } /// Get a raw data record. /// The mod manifest whose fields to match. /// The mod's default display name. /// The raw mod record. 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(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 != null && 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 *********/ /// A unique set of fields which identifies the mod. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] private class FieldSnapshot { /********* ** Accessors *********/ /// The unique mod ID (or null to ignore it). public string ID { get; set; } /// The entry DLL (or null to ignore it). public string EntryDll { get; set; } /// The mod name (or null to ignore it). public string Name { get; set; } /// The author name (or null to ignore it). public string Author { get; set; } } } }