/// <summary>The event called after a save slot is loaded.</summary> /// <param name="sender">The event sender.</param> /// <param name="e">The event arguments.</param> private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) { // load legacy data if (Context.IsMainPlayer) { this.LoadLegacyData(); } // check if mod should be enabled for the current player this.IsEnabled = Context.IsMainPlayer; if (!this.IsEnabled) { ISemanticVersion hostVersion = this.Helper.Multiplayer.GetConnectedPlayer(Game1.MasterPlayer.UniqueMultiplayerID)?.GetMod(this.ModManifest.UniqueID)?.Version; if (hostVersion == null) { this.IsEnabled = false; this.Monitor.Log("This mod is disabled because the host player doesn't have it installed.", LogLevel.Warn); } else if (hostVersion.IsOlderThan(this.MinHostVersion)) { this.IsEnabled = false; this.Monitor.Log($"This mod is disabled because the host player has {this.ModManifest.Name} {hostVersion}, but the minimum compatible version is {this.MinHostVersion}.", LogLevel.Warn); } else { this.IsEnabled = true; } } }
/********* ** Public methods *********/ /// <summary>Set backwards-compatible fields.</summary> /// <param name="version">The requested API version.</param> public void SetBackwardsCompatibility(ISemanticVersion version) { if (version.IsOlderThan("2.6-beta.19")) { this.Version = this.Main?.Version?.ToString(); this.Url = this.Main?.Url; this.PreviewVersion = this.Optional?.Version?.ToString(); this.PreviewUrl = this.Optional?.Url; } }
/// <summary> /// Apply rewrites for fields in patches /// </summary> /// <param name="change"></param> /// <param name="formatVersion"></param> /// <returns></returns> private List <string> ApplyPatchRewrites(LegacyChanges change, ISemanticVersion formatVersion) { List <string> notices = new List <string>(); if (formatVersion.IsOlderThan("1.2") && !string.IsNullOrEmpty(change.Locale)) { // Locales exists in format version 1.2 and newer. For older formats is locale undefined change.Locale = null; notices.Add($"Ignore field `Locale` in format version `{formatVersion}`"); } if (formatVersion.IsOlderThan("1.2") && !string.IsNullOrEmpty(change.LogName)) { // Locales exists in format version 1.2 and newer. For older formats is locale undefined change.LogName = null; notices.Add($"Ignore field `LogName` in format version `{formatVersion}`"); } if (!formatVersion.IsOlderThan("1.3") && string.IsNullOrEmpty(change.Action)) { change.Action = "Patch"; // Action patch is a default action in format >=1.3 } if (formatVersion.IsOlderThan("1.3") && (change.Action == "Load" || change.Action == "Edit")) { var replace = change.Action == "Load" ? "Replace" : "Patch"; notices.Add($"Rewrite action `{change.Action}` -> `{replace}`"); change.Action = replace; } if (change.Action == "Replace") { notices.Add($"Detected content replacer `{change.LogName}` for `{change.Target}`"); } return(notices); }
/********* ** Public methods *********/ /// <summary>Get whether the specified version is compatible according to this metadata.</summary> /// <param name="version">The current version of the matching mod.</param> public bool IsCompatible(ISemanticVersion version) { ISemanticVersion lowerVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; ISemanticVersion upperVersion = new SemanticVersion(this.UpperVersion); // ignore versions not in range if (lowerVersion != null && version.IsOlderThan(lowerVersion)) { return(true); } if (version.IsNewerThan(upperVersion)) { return(true); } // allow versions matching override return(!string.IsNullOrWhiteSpace(this.ForceCompatibleVersion) && Regex.IsMatch(version.ToString(), this.ForceCompatibleVersion, RegexOptions.IgnoreCase)); }
/// <summary>Register API stuff for Generic Mod Config Menu.</summary> internal static void SetUpMenu() { var api = Helper.ModRegistry.GetApi <GenericModConfigMenu.IApi> ("spacechase0.GenericModConfigMenu"); if (api == null) { return; } ISemanticVersion gmcm = Helper.ModRegistry.Get("spacechase0.GenericModConfigMenu").Manifest.Version; ISemanticVersion req = new SemanticVersion("1.1.0"); if (gmcm.IsOlderThan(req)) { Monitor.Log($"Installed version {gmcm} of GMCM is not supported. Please update Generic Mod Config Menu to version {req} or newer to enable its features for Dynamic Conversation Topics mod.", LogLevel.Warn); return; } var manifest = ModEntry.Instance.ModManifest; api.RegisterModConfig(manifest, Reset, Save); api.RegisterLabel(manifest, //i18n.Get("DialogueOptions.title"), "Debugging Tools", ""); api.RegisterSimpleOption(manifest, "Debug mode", //i18n.Get("GenderNeutrality.name"), "Visible console logging, conversation topic alerts, and dialogue box labels", //i18n.Get("GenderNeutrality.description"), () => Instance.DebugMode, (bool val) => Instance.DebugMode = val); Monitor.Log("Added DCT config to GMCM", LogLevel.Info); }
/**** ** Condition parsing ****/ /// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> public bool TryParseConditions(InvariantDictionary <string> raw, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, out ConditionDictionary conditions, out string error) { // no conditions if (raw == null || !raw.Any()) { conditions = this.ConditionFactory.BuildEmpty(); error = null; return(true); } // parse conditions conditions = this.ConditionFactory.BuildEmpty(); foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!ConditionKey.TryParse(pair.Key, out ConditionKey key)) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", Enum.GetValues(typeof(ConditionType)))}"; conditions = null; return(false); } // validate types which require an ID if (this.TypesRequireID.Contains(key.Type) && string.IsNullOrWhiteSpace(key.ForID)) { error = $"{key.Type} conditions must specify a separate ID (see readme for usage)"; conditions = null; return(false); } // check compatibility foreach (var versionPair in this.MinimumVersions) { if (formatVersion.IsOlderThan(versionPair.Key) && versionPair.Value.Contains(key.Type)) { error = $"{key} 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 = $"{key} can't be empty"; conditions = null; return(false); } // restrict to allowed values string[] rawValidValues = this.ConditionFactory.GetValidValues(key)?.ToArray(); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {key} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // create condition conditions[key] = new Condition(key, values); } // return parsed conditions error = null; return(true); }
/// <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 a given version is contained within this compatibility range.</summary> /// <param name="version">The version to check.</param> public bool MatchesVersion(ISemanticVersion version) { return ((this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion))); }
/********* ** 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(); if (rawSchema == null || !rawSchema.Any()) { return(schema); } foreach (string rawKey in rawSchema.Keys) { ConfigSchemaFieldConfig?field = rawSchema[rawKey]; if (field is null) { continue; } // 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 IInvariantSet allowValues = this.ParseCommaDelimitedField(field.AllowValues); IInvariantSet 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 = InvariantSets.FromValue(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()) { IInvariantSet invalidValues = defaultValues.GetWithout(allowValues); 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: allowValues, defaultValues: defaultValues, value: InvariantSets.Empty, allowBlank: field.AllowBlank, allowMultiple: field.AllowMultiple, description: field.Description, section: field.Section ); } return(schema); }
/**** ** Condition parsing ****/ /// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> public bool TryParseConditions(InvariantDictionary <string> raw, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, out ConditionDictionary conditions, out string error) { // no conditions if (raw == null || !raw.Any()) { conditions = this.ConditionFactory.BuildEmpty(); error = null; return(true); } // parse conditions conditions = this.ConditionFactory.BuildEmpty(); foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!Enum.TryParse(pair.Key, true, out ConditionKey key)) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", this.ConditionFactory.GetValidConditions())}"; conditions = null; return(false); } // check compatibility if (formatVersion.IsOlderThan("1.4")) { if (key == ConditionKey.DayEvent || key == ConditionKey.HasFlag || key == ConditionKey.HasSeenEvent || key == ConditionKey.Spouse) { error = $"{key} 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 = $"{key} can't be empty"; conditions = null; return(false); } // restrict to allowed values string[] rawValidValues = this.ConditionFactory.GetValidValues(key)?.ToArray(); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {key} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // create condition conditions[key] = new Condition(key, values); } // return parsed conditions error = null; return(true); }