/// <summary>Load config values from the content pack.</summary> /// <param name="contentPack">The content pack whose config file to read.</param> /// <param name="config">The config schema.</param> /// <param name="logWarning">The callback to invoke on each validation warning, passed the field name and reason respectively.</param> private void LoadConfigValues(IContentPack contentPack, InvariantDictionary <ConfigField> config, Action <string, string> logWarning) { if (!config.Any()) { return; } // read raw config InvariantDictionary <InvariantHashSet> configValues = new InvariantDictionary <InvariantHashSet>( from entry in (contentPack.ReadJsonFile <InvariantDictionary <string> >(this.Filename) ?? new InvariantDictionary <string>()) let key = entry.Key.Trim() let value = this.ParseCommaDelimitedField(entry.Value) select new KeyValuePair <string, InvariantHashSet>(key, value) ); // remove invalid values foreach (string key in configValues.Keys.ExceptIgnoreCase(config.Keys).ToArray()) { logWarning(key, "no such field supported by this content pack."); configValues.Remove(key); } // inject default values foreach (string key in config.Keys) { ConfigField field = config[key]; if (!configValues.TryGetValue(key, out InvariantHashSet values) || (!field.AllowBlank && !values.Any())) { configValues[key] = field.DefaultValues; } } // parse each field foreach (string key in config.Keys) { // set value ConfigField field = config[key]; field.Value = configValues[key]; // validate allow-multiple if (!field.AllowMultiple && field.Value.Count > 1) { logWarning(key, "field only allows a single value."); field.Value = field.DefaultValues; continue; } // validate allow-values if (field.AllowValues.Any()) { string[] invalidValues = field.Value.ExceptIgnoreCase(field.AllowValues).ToArray(); if (invalidValues.Any()) { logWarning(key, $"found invalid values ({string.Join(", ", invalidValues)}), expected: {string.Join(", ", field.AllowValues)}."); field.Value = field.DefaultValues; } } } }
/// <inheritdoc /> public override void Handle(string[] args) { InvariantDictionary <ICommand> commands = this.GetCommands(); // build output StringBuilder help = new(); if (!args.Any()) { help.AppendLine( $"The '{this.RootName}' command is the entry point for {this.ModName} commands. You use it by specifying a more " + $"specific command (like '{GenericHelpCommand.CommandName}' in '{this.RootName} {GenericHelpCommand.CommandName}'). Here are the available commands:\n\n" ); foreach (var entry in commands.OrderBy(p => p.Key, HumanSortComparer.DefaultIgnoreCase)) { help.AppendLine(entry.Value.Description); help.AppendLine(); help.AppendLine(); } } else if (commands.TryGetValue(args[0], out ICommand? command)) { help.AppendLine(command.Description); } else { help.AppendLine($"Unknown command '{this.RootName} {args[0]}'. Type '{this.RootName} {GenericHelpCommand.CommandName}' for available commands."); } // write output this.Monitor.Log(help.ToString().Trim(), LogLevel.Info); }
/// <inheritdoc /> public override void Handle(string[] args) { InvariantDictionary <ICommand> commands = this.GetCommands(); // build output StringBuilder help = new(); if (!args.Any()) { help.AppendLine( "The 'patch' command is the entry point for Content Patcher commands. These are " + "intended for troubleshooting and aren't intended for players. You use it by specifying a more " + "specific command (like 'help' in 'patch help'). Here are the available commands:\n\n" ); foreach (var entry in commands.OrderByHuman(p => p.Key)) { help.AppendLine(entry.Value.Description); help.AppendLine(); help.AppendLine(); } } else if (commands.TryGetValue(args[0], out ICommand command)) { help.AppendLine(command.Description); } else { help.AppendLine($"Unknown command 'patch {args[0]}'. Type 'patch help' for available commands."); } // write output this.Monitor.Log(help.ToString().Trim(), LogLevel.Info); }
/// <summary>Get a new string with tokens substituted.</summary> /// <param name="raw">The raw string before token substitution.</param> /// <param name="tokens">The token values to apply.</param> private string Apply(string raw, InvariantDictionary <string> tokens) { return(this.TokenPattern.Replace(raw, match => { string key = match.Groups[1].Value.Trim(); return tokens.TryGetValue(key, out string value) ? value : match.Value; })); }
/********* ** Private methods *********/ /**** ** Commands ****/ /// <summary>Handle the 'patch help' command.</summary> /// <param name="args">The subcommand arguments.</param> /// <returns>Returns whether the command was handled.</returns> private bool HandleHelp(string[] args) { // generate command info var helpEntries = new InvariantDictionary <string> { ["help"] = $"{this.CommandName} help\n Usage: {this.CommandName} help\n Lists all available {this.CommandName} commands.\n\n Usage: {this.CommandName} help <cmd>\n Provides information for a specific {this.CommandName} command.\n - cmd: The {this.CommandName} command name.", ["dump"] = $"{this.CommandName} dump\n Usage: {this.CommandName} dump order\n Lists every loaded patch in their apply order. When two patches edit the same asset, they'll be applied in the apply order.", ["export"] = $"{this.CommandName} export\n Usage: {this.CommandName} export \"<asset name>\"\n Saves a copy of an asset (including any changes from mods like Content Patcher) to the game folder. The asset name should be the target without the locale or extension, like \"Characters/Abigail\" if you want to export the value of 'Content/Characters/Abigail.xnb'.", ["parse"] = $"{this.CommandName} parse\n usage: {this.CommandName} parse \"value\"\n Parses the given token string and shows the result. For example, `{this.CommandName} parse \"assets/{{{{Season}}}}.png\" will show a value like \"assets/Spring.png\".\n\n{this.CommandName} parse \"value\" \"content-pack.id\"\n Parses the given token string and shows the result, using tokens available to the specified content pack (using the ID from the content pack's manifest.json). For example, `{this.CommandName} parse \"assets/{{{{CustomToken}}}}.png\" \"Pathoschild.ExampleContentPack\".", ["reload"] = $"{this.CommandName} reload\n Usage: {this.CommandName} reload \"<content pack ID>\"\n Reloads the patches of the content.json of a content pack. Config schema changes and dynamic token changes are unsupported.", ["summary"] = $"{this.CommandName} summary\n Usage: {this.CommandName} summary\n Shows a summary of the current conditions and loaded patches.\n\n Usage: {this.CommandName} summary \"<content pack ID>\"\n Show a summary of the current conditions, and loaded patches for the given content pack.", ["update"] = $"{this.CommandName} update\n Usage: {this.CommandName} update\n Immediately refreshes the condition context and rechecks all patches." }; // build output StringBuilder help = new StringBuilder(); if (!args.Any()) { help.AppendLine( $"The '{this.CommandName}' command is the entry point for Content Patcher commands. These are " + "intended for troubleshooting and aren't intended for players. You use it by specifying a more " + $"specific command (like 'help' in '{this.CommandName} help'). Here are the available commands:\n\n" ); foreach (var entry in helpEntries.OrderByIgnoreCase(p => p.Key)) { help.AppendLine(entry.Value); help.AppendLine(); help.AppendLine(); } } else if (helpEntries.TryGetValue(args[0], out string entry)) { help.AppendLine(entry); } else { help.AppendLine($"Unknown command '{this.CommandName} {args[0]}'. Type '{this.CommandName} help' for available commands."); } // write output this.Monitor.Log(help.ToString().Trim(), LogLevel.Debug); return(true); }
/********* ** Private methods *********/ /**** ** Commands ****/ /// <summary>Handle the 'patch help' command.</summary> /// <param name="args">The subcommand arguments.</param> /// <returns>Returns whether the command was handled.</returns> private bool HandleHelp(string[] args) { // generate command info var helpEntries = new InvariantDictionary <string> { ["help"] = $"{this.CommandName} help\n Usage: {this.CommandName} help\n Lists all available {this.CommandName} commands.\n\n Usage: {this.CommandName} help <cmd>\n Provides information for a specific {this.CommandName} command.\n - cmd: The {this.CommandName} command name.", ["summary"] = $"{this.CommandName} summary\n Usage: {this.CommandName} summary\n Shows a summary of the current conditions and loaded patches.", ["update"] = $"{this.CommandName} update\n Usage: {this.CommandName} update\n Imediately refreshes the condition context and rechecks all patches.", ["export"] = $"{this.CommandName} export\n Usage: {this.CommandName} export \"<asset name>\"\n Saves a copy of an asset (including any changes from mods like Content Patcher) to the game folder. The asset name should be the target without the locale or extension, like \"Characters/Abigail\" if you want to export the value of 'Content/Characters/Abigail.xnb'." }; // build output StringBuilder help = new StringBuilder(); if (!args.Any()) { help.AppendLine( $"The '{this.CommandName}' command is the entry point for Content Patcher commands. These are " + "intended for troubleshooting and aren't intended for players. You use it by specifying a more " + $"specific command (like 'help' in '{this.CommandName} help'). Here are the available commands:\n\n" ); foreach (var entry in helpEntries.OrderByIgnoreCase(p => p.Key)) { help.AppendLine(entry.Value); help.AppendLine(); } } else if (helpEntries.TryGetValue(args[0], out string entry)) { help.AppendLine(entry); } else { help.AppendLine($"Unknown command '{this.CommandName} {args[0]}'. Type '{this.CommandName} help' for available commands."); } // write output this.Monitor.Log(help.ToString()); return(true); }
/********* ** Private methods *********/ /**** ** Commands ****/ /// <summary>Handle the 'patch help' command.</summary> /// <param name="args">The subcommand arguments.</param> /// <returns>Returns whether the command was handled.</returns> private bool HandleHelp(string[] args) { // generate command info var helpEntries = new InvariantDictionary <string> { ["help"] = $"{this.CommandName} help\n Usage: {this.CommandName} help\n Lists all available {this.CommandName} commands.\n\n Usage: {this.CommandName} help <cmd>\n Provides information for a specific {this.CommandName} command.\n - cmd: The {this.CommandName} command name.", ["summary"] = $"{this.CommandName} summary\n Usage: {this.CommandName} summary\n Shows a summary of the current conditions and loaded patches.", ["update"] = $"{this.CommandName} update\n Usage: {this.CommandName} update\n Imediately refreshes the condition context and rechecks all patches." }; // build output StringBuilder help = new StringBuilder(); if (!args.Any()) { help.AppendLine( $"The '{this.CommandName}' command is the entry point for Content Patcher commands. These are " + "intended for troubleshooting and aren't intended for players. You use it by specifying a more " + $"specific command (like 'help' in '{this.CommandName} help'). Here are the available commands:\n\n" ); foreach (var entry in helpEntries.OrderByIgnoreCase(p => p.Key)) { help.AppendLine(entry.Value); help.AppendLine(); } } else if (helpEntries.TryGetValue(args[0], out string entry)) { help.AppendLine(entry); } else { help.AppendLine($"Unknown command '{this.CommandName} {args[0]}'. Type '{this.CommandName} help' for available commands."); } // write output this.Monitor.Log(help.ToString()); return(true); }
/********* ** Private methods *********/ /// <summary>Handle the 'data-layers help' command.</summary> /// <param name="args">The subcommand arguments.</param> /// <returns>Returns whether the command was handled.</returns> private bool HandleHelp(string[] args) { // generate command info var helpEntries = new InvariantDictionary <string> { ["help"] = $"{this.CommandName} help\n Usage: {this.CommandName} help\n Lists all available {this.CommandName} commands.\n\n Usage: {this.CommandName} help <cmd>\n Provides information for a specific {this.CommandName} command.\n - cmd: The {this.CommandName} command name.", ["export"] = $"{this.CommandName} export\n Usage: {this.CommandName} export \"<asset name>\"\n Exports the current data layer for the entire location to the game folder." }; // build output StringBuilder help = new StringBuilder(); if (!args.Any()) { help.AppendLine( $"The '{this.CommandName}' command is the entry point for Data Layers commands. You use it by" + $"specifying a more specific command (like 'help' in '{this.CommandName} help'). Here are the" + $"available commands:\n\n" ); foreach (var entry in helpEntries) { help.AppendLine(entry.Value); help.AppendLine(); } } else if (helpEntries.TryGetValue(args[0], out string entry)) { help.AppendLine(entry); } else { help.AppendLine($"Unknown command '{this.CommandName} {args[0]}'. Type '{this.CommandName} help' for available commands."); } // write output this.Monitor.Log(help.ToString(), LogLevel.Debug); return(true); }
/// <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.Except(validValues, StringComparer.InvariantCultureIgnoreCase).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); }