Example #1
0
        /// <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;
                    }
                }
            }
        }
Example #2
0
        /// <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);
        }
Example #3
0
        /// <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);
        }
Example #4
0
 /// <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;
     }));
 }
Example #5
0
        /*********
        ** 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);
        }
Example #7
0
        /*********
        ** 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);
        }
Example #9
0
        /// <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);
        }