using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace StardewModdingAPI.Framework.Models
{
/// Raw mod metadata from SMAPI's internal mod list.
internal class ModDataRecord
{
/*********
** Properties
*********/
/// This field stores properties that aren't mapped to another field before they're parsed into .
[JsonExtensionData]
private IDictionary ExtensionData;
/*********
** Accessors
*********/
/// The mod's current unique ID.
public string ID { get; set; }
/// The former mod IDs (if any).
///
/// 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
/// | 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.
///
public string FormerIDs { get; set; }
/// Maps local versions to a semantic version for update checks.
public IDictionary MapLocalVersions { get; set; } = new Dictionary();
/// Maps remote versions to a semantic version for update checks.
public IDictionary MapRemoteVersions { get; set; } = new Dictionary();
/// The versioned field data.
///
/// This maps field names to values. This should be accessed via .
/// Format notes:
/// - Each key consists of a field name prefixed with any combination of version range
/// and Default, separated by pipes (whitespace trimmed). For example, Name
/// will always override the name, Default | Name will only override a blank
/// name, and ~1.1 | Default | Name will override blank names up to version 1.1.
/// - The version format is min~max (where either side can be blank for unbounded), or
/// a single version number.
/// - The field name itself corresponds to a value.
///
public IDictionary Fields { get; set; } = new Dictionary();
/*********
** Public methods
*********/
/// Get whether the manifest matches the field.
/// The mod manifest to check.
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(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;
}
/// Get a parsed representation of the .
public IEnumerable GetFields()
{
foreach (KeyValuePair 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);
}
}
/// Get a parsed representation of the which match a given manifest.
/// The manifest to match.
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;
}
/// Get a semantic local version for update checks.
/// The remote version to normalise.
public string GetLocalVersionForUpdateChecks(string version)
{
return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion)
? newVersion
: version;
}
/// Get a semantic remote version for update checks.
/// The remote version to normalise.
public string GetRemoteVersionForUpdateChecks(string version)
{
return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion)
? newVersion
: version;
}
/*********
** Private methods
*********/
/// The method invoked after JSON deserialisation.
/// The deserialisation context.
[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
*********/
/// 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; }
}
}
}