/********* ** Private methods *********/ /// <summary>Get the mod version numbers for the given mod.</summary> /// <param name="mod">The mod to check.</param> /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param> /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param> /// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param> /// <param name="main">The main mod version.</param> /// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param> private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary <string, string> mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) { main = null; preview = null; ISemanticVersion ParseVersion(string raw) { raw = this.NormalizeVersion(raw); return(this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions)); } if (mod != null) { // get mod version if (subkey == null) { main = ParseVersion(mod.Version); } // get file versions foreach (IModDownload download in mod.Downloads) { // check for subkey if specified if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true) { continue; } // parse version ISemanticVersion cur = ParseVersion(download.Version); if (cur == null) { continue; } // track highest versions if (main == null || cur.IsNewerThan(main)) { main = cur; } if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview))) { preview = cur; } } if (preview != null && !preview.IsNewerThan(main)) { preview = null; } } return(main != null); }
/********* ** 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>Construct an instance.</summary> /// <param name="name">The mod name.</param> /// <param name="version">The semantic version for the mod's latest release.</param> /// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param> /// <param name="url">The mod's web URL.</param> public ModInfoModel(string name, ISemanticVersion version, string url, ISemanticVersion previewVersion = null) { this .SetBasicInfo(name, url) .SetVersions(version, previewVersion); }
// Constructors: #region Constructors internal CompatibilityPatch(String uid, ISemanticVersion version) { this.uid = uid; this.version = version; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="baseUrl">The base URL for the web API.</param> /// <param name="version">The web API version.</param> public WebApiClient(string baseUrl, ISemanticVersion version) { this.BaseUrl = new Uri(baseUrl); this.Version = version; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="mod">The mod metadata.</param> public MultiplayerPeerMod(RemoteContextModModel mod) { this.Name = mod.Name; this.ID = mod.ID?.Trim(); this.Version = mod.Version; }
/********* ** Private methods *********/ /// <summary>Parse a raw config schema for a content pack.</summary> /// <param name="rawSchema">The raw config schema.</param> /// <param name="logWarning">The callback to invoke on each validation warning, passed the field name and reason respectively.</param> /// <param name="formatVersion">The content format version.</param> private InvariantDictionary <ConfigField> LoadConfigSchema(InvariantDictionary <ConfigSchemaFieldConfig> rawSchema, Action <string, string> logWarning, ISemanticVersion formatVersion) { InvariantDictionary <ConfigField> schema = new InvariantDictionary <ConfigField>(); if (rawSchema == null || !rawSchema.Any()) { return(schema); } foreach (string rawKey in rawSchema.Keys) { ConfigSchemaFieldConfig field = rawSchema[rawKey]; // validate format if (string.IsNullOrWhiteSpace(rawKey)) { logWarning(rawKey, "the config field name can't be empty."); continue; } if (rawKey.Contains(InternalConstants.PositionalInputArgSeparator) || rawKey.Contains(InternalConstants.NamedInputArgSeparator)) { logWarning(rawKey, $"the name '{rawKey}' can't have input arguments ({InternalConstants.PositionalInputArgSeparator} or {InternalConstants.NamedInputArgSeparator} character)."); continue; } // validate reserved keys if (Enum.TryParse <ConditionType>(rawKey, true, out _)) { logWarning(rawKey, $"can't use {rawKey} as a config field, because it's a reserved condition key."); continue; } // read allowed/default values InvariantHashSet allowValues = this.ParseCommaDelimitedField(field.AllowValues); InvariantHashSet defaultValues = this.ParseCommaDelimitedField(field.Default); // pre-1.7 behaviour if (formatVersion.IsOlderThan("1.7")) { // allowed values are required if (!allowValues.Any()) { logWarning(rawKey, $"no {nameof(ConfigSchemaFieldConfig.AllowValues)} specified (and format version is less than 1.7)."); continue; } // inject default if needed if (!defaultValues.Any() && !field.AllowBlank) { defaultValues = new InvariantHashSet(allowValues.First()); } } // validate allowed values if (!field.AllowBlank && !defaultValues.Any()) { logWarning(rawKey, $"if {nameof(field.AllowBlank)} is false, you must specify {nameof(field.Default)}."); continue; } if (allowValues.Any() && defaultValues.Any()) { string[] invalidValues = defaultValues.ExceptIgnoreCase(allowValues).ToArray(); if (invalidValues.Any()) { logWarning(rawKey, $"default values '{string.Join(", ", invalidValues)}' are not allowed according to {nameof(ConfigSchemaFieldConfig.AllowValues)}."); continue; } } // validate allow multiple if (!field.AllowMultiple && defaultValues.Count > 1) { logWarning(rawKey, $"can't have multiple default values because {nameof(ConfigSchemaFieldConfig.AllowMultiple)} is false."); continue; } // add to schema schema[rawKey] = new ConfigField(allowValues, defaultValues, field.AllowBlank, field.AllowMultiple); } return(schema); }
/********* ** Private methods *********/ /// <summary>Get the mod version numbers for the given mod.</summary> /// <param name="mod">The mod to check.</param> /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param> /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param> /// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param> /// <param name="main">The main mod version.</param> /// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param> private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) { main = null; preview = null; // parse all versions from the mod page IEnumerable <(string name, string description, ISemanticVersion version)> GetAllVersions() { if (mod != null) { ISemanticVersion ParseAndMapVersion(string raw) { raw = this.NormalizeVersion(raw); return(this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions)); } // get mod version ISemanticVersion modVersion = ParseAndMapVersion(mod.Version); if (modVersion != null) { yield return(name : null, description : null, version : ParseAndMapVersion(mod.Version)); } // get file versions foreach (IModDownload download in mod.Downloads) { ISemanticVersion cur = ParseAndMapVersion(download.Version); if (cur != null) { yield return(download.Name, download.Description, cur); } } } } var versions = GetAllVersions() .OrderByDescending(p => p.version, SemanticVersionComparer.Instance) .ToArray(); // get main + preview versions void TryGetVersions(out ISemanticVersion mainVersion, out ISemanticVersion previewVersion, Func <(string name, string description, ISemanticVersion version), bool> filter = null) { mainVersion = null; previewVersion = null; // get latest main + preview version foreach (var entry in versions) { if (filter?.Invoke(entry) == false) { continue; } if (entry.version.IsPrerelease()) { previewVersion ??= entry.version; } else { mainVersion ??= entry.version; } if (mainVersion != null) { break; // any other values will be older } } // normalize values if (previewVersion is not null) { mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version if (!previewVersion.IsNewerThan(mainVersion)) { previewVersion = null; } } } if (subkey is not null) { TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true); } if (main is null) { TryGetVersions(out main, out preview); } return(main != null); }
/// <summary>Get the mods for which the API should return data.</summary> /// <param name="model">The search model.</param> /// <param name="apiVersion">The requested API version.</param> private IEnumerable <ModSearchEntryModel> GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion) { if (model == null) { yield break; } // yield standard entries if (model.Mods != null) { foreach (ModSearchEntryModel mod in model.Mods) { yield return(mod); } } // yield mod update keys if backwards compatible if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17")) { foreach (string updateKey in model.ModKeys.Distinct()) { yield return(new ModSearchEntryModel(updateKey, new[] { updateKey })); } } }
/// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion, Func <string, string> getUpdateUrl) { mods = mods.ToArray(); // validate each manifest foreach (IModMetadata mod in mods) { // skip if already failed if (mod.Status == ModMetadataStatus.Failed) { continue; } // validate compatibility from internal data switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's outdated"; // get update URLs List <string> updateUrls = new List <string>(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { string url = getUpdateUrl(key); if (url != null) { updateUrls.Add(url); } } if (mod.DataRecord.AlternativeUrl != null) { updateUrls.Add(mod.DataRecord.AlternativeUrl); } // default update URL updateUrls.Add("https://smapi.io/compat"); // build error string error = $"{reasonPhrase}. Please check for a "; if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) { error += "newer version"; } else { error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; } error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); } continue; } // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } // validate DLL / content pack fields { bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); bool isContentPack = mod.Manifest.ContentPackFor != null; // validate field presence if (!hasDll && !isContentPack) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); continue; } if (hasDll && isContentPack) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); continue; } // validate DLL if (hasDll) { // invalid filename format if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); continue; } // invalid path string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } } // validate content pack else { // invalid content pack ID if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); continue; } } } // validate required fields { List <string> missingFields = new List <string>(3); if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) { missingFields.Add(nameof(IManifest.Name)); } if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") { missingFields.Add(nameof(IManifest.Version)); } if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) { missingFields.Add(nameof(IManifest.UniqueID)); } if (missingFields.Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); } } } // validate IDs are unique { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { foreach (IModMetadata mod in group) { if (mod.Status == ModMetadataStatus.Failed) { continue; // don't replace metadata error } mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))})."); } } } }
public SaveManager(ISemanticVersion version, CategorizeChestsModule module) { _version = version; _module = module; }
/// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="tokenContext">The tokens available for this content pack.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="minumumTokenVersions">The minimum format versions for newer condition types.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> private bool TryParseConditions(InvariantDictionary <string> raw, IContext tokenContext, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, InvariantDictionary <ISemanticVersion> minumumTokenVersions, out ConditionDictionary conditions, out string error) { conditions = new ConditionDictionary(); // no conditions if (raw == null || !raw.Any()) { error = null; return(true); } // parse conditions foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!TokenName.TryParse(pair.Key, out TokenName name)) { error = $"'{pair.Key}' isn't a valid token name"; conditions = null; return(false); } // get token IToken token = tokenContext.GetToken(name, enforceContext: false); if (token == null) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", tokenContext.GetTokens(enforceContext: false).Select(p => p.Name).OrderBy(p => p))}"; conditions = null; return(false); } // validate subkeys if (!token.CanHaveSubkeys) { if (name.HasSubkey()) { error = $"{name.Key} conditions don't allow subkeys (:)"; conditions = null; return(false); } } else if (token.RequiresSubkeys) { if (!name.HasSubkey()) { error = $"{name.Key} conditions must specify a token subkey (see readme for usage)"; conditions = null; return(false); } } // check compatibility if (minumumTokenVersions.TryGetValue(name.Key, out ISemanticVersion minVersion) && minVersion.IsNewerThan(formatVersion)) { error = $"{name} isn't available with format version {formatVersion} (change the {nameof(ContentConfig.Format)} field to {latestFormatVersion} to use newer features)"; conditions = null; return(false); } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{name} can't be empty"; conditions = null; return(false); } // restrict to allowed values InvariantHashSet rawValidValues = token.GetAllowedValues(name); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.ExceptIgnoreCase(validValues).ToArray(); if (invalidValues.Any()) { error = $"invalid {name} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // perform custom validation if (!token.TryCustomValidation(values, out string customError)) { error = $"invalid {name} values: {customError}"; conditions = null; return(false); } // create condition conditions[name] = new Condition(name, values); } // return parsed conditions error = null; return(true); }
/// <summary>Load one patch from a content pack's <c>content.json</c> file.</summary> /// <param name="pack">The content pack being loaded.</param> /// <param name="contentConfig">The content pack's config.</param> /// <param name="entry">The change to load.</param> /// <param name="tokenContext">The tokens available for this content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="minumumTokenVersions">The minimum format versions for newer condition types.</param> /// <param name="logSkip">The callback to invoke with the error reason if loading it fails.</param> private bool LoadPatch(ManagedContentPack pack, ContentConfig contentConfig, PatchConfig entry, IContext tokenContext, ISemanticVersion latestFormatVersion, InvariantDictionary <ISemanticVersion> minumumTokenVersions, Action <string> logSkip) { bool TrackSkip(string reason, bool warn = true) { this.PatchManager.AddPermanentlyDisabled(new DisabledPatch(entry.LogName, entry.Action, entry.Target, pack, reason)); if (warn) { logSkip(reason); } return(false); } try { // normalise patch fields if (entry.When == null) { entry.When = new InvariantDictionary <string>(); } // parse action if (!Enum.TryParse(entry.Action, true, out PatchType action)) { return(TrackSkip(string.IsNullOrWhiteSpace(entry.Action) ? $"must set the {nameof(PatchConfig.Action)} field." : $"invalid {nameof(PatchConfig.Action)} value '{entry.Action}', expected one of: {string.Join(", ", Enum.GetNames(typeof(PatchType)))}." )); } // parse target asset TokenString assetName; { if (string.IsNullOrWhiteSpace(entry.Target)) { return(TrackSkip($"must set the {nameof(PatchConfig.Target)} field.")); } if (!this.TryParseTokenString(entry.Target, tokenContext, out string error, out assetName)) { return(TrackSkip($"the {nameof(PatchConfig.Target)} is invalid: {error}")); } } // parse 'enabled' bool enabled = true; { if (entry.Enabled != null && !this.TryParseEnabled(entry.Enabled, tokenContext, out string error, out enabled)) { return(TrackSkip($"invalid {nameof(PatchConfig.Enabled)} value '{entry.Enabled}': {error}")); } } // parse conditions ConditionDictionary conditions; { if (!this.TryParseConditions(entry.When, tokenContext, contentConfig.Format, latestFormatVersion, minumumTokenVersions, out conditions, out string error)) { return(TrackSkip($"the {nameof(PatchConfig.When)} field is invalid: {error}.")); } } // get patch instance IPatch patch; switch (action) { // load asset case PatchType.Load: { // init patch if (!this.TryPrepareLocalAsset(pack, entry.FromFile, tokenContext, out string error, out TokenString fromAsset)) { return(TrackSkip(error)); } patch = new LoadPatch(entry.LogName, pack, assetName, conditions, fromAsset, this.Helper.Content.NormaliseAssetName); } break; // edit data case PatchType.EditData: { // validate if (entry.Entries == null && entry.Fields == null) { return(TrackSkip($"either {nameof(PatchConfig.Entries)} or {nameof(PatchConfig.Fields)} must be specified for a '{action}' change.")); } if (entry.Entries != null && entry.Entries.Any(p => p.Value != null && p.Value.Trim() == "")) { return(TrackSkip($"the {nameof(PatchConfig.Entries)} can't contain empty values.")); } if (entry.Fields != null && entry.Fields.Any(p => p.Value == null || p.Value.Any(n => n.Value == null))) { return(TrackSkip($"the {nameof(PatchConfig.Fields)} can't contain empty values.")); } // parse entries List <EditDataPatchRecord> entries = new List <EditDataPatchRecord>(); if (entry.Entries != null) { foreach (KeyValuePair <string, string> pair in entry.Entries) { if (!this.TryParseTokenString(pair.Key, tokenContext, out string keyError, out TokenString key)) { return(TrackSkip($"the {nameof(PatchConfig.Entries)} > '{key}' entry key is invalid: {keyError}.")); } if (!this.TryParseTokenString(pair.Value, tokenContext, out string error, out TokenString value)) { return(TrackSkip($"the {nameof(PatchConfig.Entries)} > '{key}' entry value is invalid: {error}.")); } entries.Add(new EditDataPatchRecord(key, value)); } } // parse fields List <EditDataPatchField> fields = new List <EditDataPatchField>(); if (entry.Fields != null) { foreach (KeyValuePair <string, IDictionary <int, string> > recordPair in entry.Fields) { if (!this.TryParseTokenString(recordPair.Key, tokenContext, out string keyError, out TokenString key)) { return(TrackSkip($"the {nameof(PatchConfig.Fields)} > '{keyError}' field key is invalid: {keyError}.")); } foreach (var fieldPair in recordPair.Value) { int field = fieldPair.Key; if (!this.TryParseTokenString(fieldPair.Value, tokenContext, out string valueError, out TokenString value)) { return(TrackSkip($"the {nameof(PatchConfig.Fields)} > '{key}' > {field} field is invalid: {valueError}.")); } fields.Add(new EditDataPatchField(key, field, value)); } } } // save patch = new EditDataPatch(entry.LogName, pack, assetName, conditions, entries, fields, this.Monitor, this.Helper.Content.NormaliseAssetName); } break; // edit image case PatchType.EditImage: { // read patch mode PatchMode patchMode = PatchMode.Replace; if (!string.IsNullOrWhiteSpace(entry.PatchMode) && !Enum.TryParse(entry.PatchMode, true, out patchMode)) { return(TrackSkip($"the {nameof(PatchConfig.PatchMode)} is invalid. Expected one of these values: [{string.Join(", ", Enum.GetNames(typeof(PatchMode)))}].")); } // save if (!this.TryPrepareLocalAsset(pack, entry.FromFile, tokenContext, out string error, out TokenString fromAsset)) { return(TrackSkip(error)); } patch = new EditImagePatch(entry.LogName, pack, assetName, conditions, fromAsset, entry.FromArea, entry.ToArea, patchMode, this.Monitor, this.Helper.Content.NormaliseAssetName); } break; default: return(TrackSkip($"unsupported patch type '{action}'.")); } // skip if not enabled // note: we process the patch even if it's disabled, so any errors are caught by the modder instead of only failing after the patch is enabled. if (!enabled) { return(TrackSkip($"{nameof(PatchConfig.Enabled)} is false.", warn: false)); } // save patch this.PatchManager.Add(patch); return(true); } catch (Exception ex) { return(TrackSkip($"error reading info. Technical details:\n{ex}")); } }
/// <summary>Construct an instance.</summary> /// <param name="id">The unique mod ID.</param> /// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param> /// <param name="updateKeys">The namespaced mod update keys (if available).</param> /// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param> public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false) { this.ID = id; this.InstalledVersion = installedVersion; this.UpdateKeys = updateKeys ?? new string[0]; }
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary> /// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns> /// <param name="other">An object to compare with this object.</param> public bool Equals(ISemanticVersion other) { return(other != null && this.CompareTo(other) == 0); }
public ModApi(ModConfig config, ISemanticVersion modVersion) { this.Config = config; this.ModVersion = modVersion; }
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> /// <param name="other">The version to compare with this instance.</param> /// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception> /// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks> public int CompareTo(ISemanticVersion other) { if (other == null) { throw new ArgumentNullException(nameof(other)); } const int same = 0; const int curNewer = 1; const int curOlder = -1; // compare stable versions if (this.MajorVersion != other.MajorVersion) { return(this.MajorVersion.CompareTo(other.MajorVersion)); } if (this.MinorVersion != other.MinorVersion) { return(this.MinorVersion.CompareTo(other.MinorVersion)); } if (this.PatchVersion != other.PatchVersion) { return(this.PatchVersion.CompareTo(other.PatchVersion)); } if (this.Build == other.Build) { return(same); } // stable supercedes pre-release bool curIsStable = string.IsNullOrWhiteSpace(this.Build); bool otherIsStable = string.IsNullOrWhiteSpace(other.Build); if (curIsStable) { return(curNewer); } if (otherIsStable) { return(curOlder); } // compare two pre-release tag values string[] curParts = this.Build.Split('.', '-'); string[] otherParts = other.Build.Split('.', '-'); for (int i = 0; i < curParts.Length; i++) { // longer prerelease tag supercedes if otherwise equal if (otherParts.Length <= i) { return(curNewer); } // compare if different if (curParts[i] != otherParts[i]) { // compare numerically if possible { if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) { return(curNum.CompareTo(otherNum)); } } // else compare lexically return(string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase)); } } // fallback (this should never happen) return(string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase)); }
/// <summary>Construct an instance.</summary> /// <param name="mods">The mods to search.</param> /// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param> /// <param name="gameVersion">The Secrets Of Grindea version installed by the player.</param> /// <param name="platform">The OS on which the player plays.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata) { this.Mods = mods.ToArray(); this.ApiVersion = apiVersion; this.GameVersion = gameVersion; this.Platform = platform; this.IncludeExtendedMetadata = includeExtendedMetadata; }
/// <summary>Read the configuration file for a content pack.</summary> /// <param name="contentPack">The content pack.</param> /// <param name="rawSchema">The raw config schema from the mod's <c>content.json</c>.</param> /// <param name="formatVersion">The content format version.</param> public InvariantDictionary <ConfigField> Read(ManagedContentPack contentPack, InvariantDictionary <ConfigSchemaFieldConfig> rawSchema, ISemanticVersion formatVersion) { InvariantDictionary <ConfigField> config = this.LoadConfigSchema(rawSchema, logWarning: (field, reason) => this.LogWarning(contentPack, $"{nameof(ContentConfig.ConfigSchema)} field '{field}'", reason), formatVersion); this.LoadConfigValues(contentPack, config, logWarning: (field, reason) => this.LogWarning(contentPack, $"{this.Filename} > {field}", reason)); return(config); }
/********* ** Protected methods *********/ /// <summary>Construct an instance.</summary> /// <param name="version">The version to which this migration applies.</param> protected BaseMigration(ISemanticVersion version) { this.Version = version; }
/********* ** Private methods *********/ /// <summary>Get the metadata for a mod.</summary> /// <param name="search">The mod data to match.</param> /// <param name="wikiData">The wiki data.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <param name="apiVersion">The SMAPI version installed by the player.</param> /// <returns>Returns the mod data if found, else <c>null</c>.</returns> private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion) { // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.InvariantCultureIgnoreCase)); bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false; // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList <string> errors = new List <string>(); ModEntryVersionModel main = null; ModEntryVersionModel optional = null; ModEntryVersionModel unofficial = null; ModEntryVersionModel unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // validate update key if (!updateKey.LooksValid) { errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); continue; } // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions); if (data.Error != null) { errors.Add(data.Error); continue; } // handle main version if (data.Version != null) { ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions); if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } if (this.IsNewer(version, main?.Version)) { main = new ModEntryVersionModel(version, data.Url); } } // handle optional version if (data.PreviewVersion != null) { ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions); if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); continue; } if (this.IsNewer(version, optional?.Version)) { optional = new ModEntryVersionModel(version, data.Url); } } } // get unofficial version if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) { unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}"); } // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}") : null; } else { unofficialForBeta = unofficial; } } } // fallback to preview if latest is invalid if (main == null && optional != null) { main = optional; optional = null; } // special cases if (overrides?.SetUrl != null) { if (main != null) { main.Url = overrides.SetUrl; } if (optional != null) { optional.Url = overrides.SetUrl; } } // get recommended update (if any) ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions); if (apiVersion != null && installedVersion != null) { // get newer versions List <ModEntryVersionModel> updates = new List <ModEntryVersionModel>(); if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true)) { updates.Add(main); } if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease())) { updates.Add(optional); } if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken)) { updates.Add(unofficial); } if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) { updates.Add(unofficialForBeta); } // get newest version ModEntryVersionModel newest = null; foreach (ModEntryVersionModel update in updates) { if (newest == null || update.Version.IsNewerThan(newest.Version)) { newest = update; } } // set field result.SuggestedUpdate = newest != null ? new ModEntryVersionModel(newest.Version, newest.Url) : null; } // add extended metadata if (includeExtendedMetadata) { result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta); } // add result result.Errors = errors.ToArray(); return(result); }
/// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion, Func <string, string> getUpdateUrl) { mods = mods.ToArray(); // validate each manifest foreach (IModMetadata mod in mods) { // skip if already failed if (mod.Status == ModMetadataStatus.Failed) { continue; } // validate compatibility from internal data switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List <string> updateUrls = new List <string>(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { string url = getUpdateUrl(key); if (url != null) { updateUrls.Add(url); } } if (mod.DataRecord.AlternativeUrl != null) { updateUrls.Add(mod.DataRecord.AlternativeUrl); } // default update URL updateUrls.Add("https://smapi.io/mods"); // build error string error = $"{reasonPhrase}. Please check for a "; if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) { error += "newer version"; } else { error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; } error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); } continue; } // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } // validate DLL / content pack fields { bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); bool isContentPack = mod.Manifest.ContentPackFor != null; // validate field presence if (!hasDll && !isContentPack) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); continue; } if (hasDll && isContentPack) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); continue; } // validate DLL if (hasDll) { // invalid filename format if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); continue; } // invalid path if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll))) { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } // invalid capitalization string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; if (actualFilename != mod.Manifest.EntryDll) { mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); continue; } } // validate content pack else { // invalid content pack ID if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); continue; } } } // validate required fields { List <string> missingFields = new List <string>(3); if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) { missingFields.Add(nameof(IManifest.Name)); } if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") { missingFields.Add(nameof(IManifest.Version)); } if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) { missingFields.Add(nameof(IManifest.UniqueID)); } if (missingFields.Any()) { mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); continue; } } // validate ID format if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) { mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } } // validate IDs are unique { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.OrdinalIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { foreach (IModMetadata mod in group) { if (mod.Status == ModMetadataStatus.Failed) { continue; // don't replace metadata error } string folderList = string.Join(", ", from entry in @group let relativePath = entry.GetRelativePathWithRoot() orderby relativePath select $"{relativePath} ({entry.Manifest.Version})" ); mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed. Found in folders: {folderList}."); } } } }
/// <summary>Create a temporary content pack to read files from a directory. Temporary content packs will not appear in the SMAPI log and update checks will not be performed.</summary> /// <param name="directoryPath">The absolute directory path containing the content pack files.</param> /// <param name="id">The content pack's unique ID.</param> /// <param name="name">The content pack name.</param> /// <param name="description">The content pack description.</param> /// <param name="author">The content pack author's name.</param> /// <param name="version">The content pack version.</param> public IContentPack CreateTemporary(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) { // validate if (string.IsNullOrWhiteSpace(directoryPath)) { throw new ArgumentNullException(nameof(directoryPath)); } if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id)); } if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException(nameof(name)); } if (!Directory.Exists(directoryPath)) { throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); } // create manifest IManifest manifest = new Manifest( uniqueID: id, name: name, author: author, description: description, version: version, contentPackFor: this.ModID ); // create content pack return(this.CreateContentPack(directoryPath, manifest)); }
public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) { // raise deprecation notice this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate if (string.IsNullOrWhiteSpace(directoryPath)) { throw new ArgumentNullException(nameof(directoryPath)); } if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id)); } if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException(nameof(name)); } if (!Directory.Exists(directoryPath)) { throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); } // create manifest IManifest manifest = new Manifest { Name = name, Author = author, Description = description, Version = version, UniqueID = id, UpdateKeys = new string[0], ContentPackFor = new ManifestContentPackFor { UniqueID = this.ModID } }; // create content pack return(this.CreateContentPack(directoryPath, manifest)); }
/// <summary>Get metadata about a set of mods from the web API.</summary> /// <param name="mods">The mod keys for which to fetch the latest version.</param> /// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param> /// <param name="gameVersion">The Stardew Valley version installed by the player.</param> /// <param name="platform">The OS on which the player plays.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> public IDictionary <string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false) { return(this.Post <ModSearchModel, ModEntryModel[]>( $"v{this.Version}/mods", new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata) ).ToDictionary(p => p.ID)); }
/// <summary>Get whether this version is newer than the specified version.</summary> /// <param name="other">The version to compare with this instance.</param> public bool IsNewerThan(ISemanticVersion other) { return(this.CompareTo(other) > 0); }
/// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary> /// <param name="current">The current version.</param> /// <param name="other">The other version.</param> private bool IsNewer(ISemanticVersion current, ISemanticVersion other) { return(current != null && (other == null || other.IsOlderThan(current))); }
/// <summary>Get whether this version is between two specified versions (inclusively).</summary> /// <param name="min">The minimum version.</param> /// <param name="max">The maximum version.</param> public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { return(this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0); }
/********* ** 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 version is older than the specified version.</summary> /// <param name="other">The version to compare with this instance.</param> public bool IsOlderThan(ISemanticVersion other) { return(this.Version.IsOlderThan(other)); }