/// <summary>Get every permutation of the given values.</summary>
        /// <param name="values">The possible values.</param>
        public IEnumerable <InvariantDictionary <string> > GetPermutations(InvariantDictionary <InvariantHashSet> values)
        {
            // no permutations possible
            if (!values.Any())
            {
                return(new InvariantDictionary <string> [0]);
            }

            // recursively find permutations
            InvariantDictionary <string> curPermutation = new InvariantDictionary <string>();

            IEnumerable <InvariantDictionary <string> > GetPermutations(string[] keyQueue)
            {
                if (!keyQueue.Any())
                {
                    yield return(new InvariantDictionary <string>(curPermutation));

                    yield break;
                }

                string key = keyQueue[0];

                foreach (string value in values[key])
                {
                    curPermutation[key] = value;
                    foreach (var permutation in GetPermutations(keyQueue.Skip(1).ToArray()))
                    {
                        yield return(permutation);
                    }
                }
            }

            return(GetPermutations(values.Keys.ToArray()));
        }
Exemple #2
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;
                    }
                }
            }
        }
Exemple #3
0
        /*********
        ** Properties
        *********/
        /// <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>
        private InvariantDictionary <ConfigField> LoadConfigSchema(InvariantDictionary <ConfigSchemaFieldConfig> rawSchema, Action <string, string> logWarning)
        {
            InvariantDictionary <ConfigField> schema = new InvariantDictionary <ConfigField>();

            if (rawSchema == null || !rawSchema.Any())
            {
                return(schema);
            }

            foreach (string key in rawSchema.Keys)
            {
                ConfigSchemaFieldConfig field = rawSchema[key];

                // validate key
                if (Enum.TryParse(key, true, out ConditionKey conditionKey))
                {
                    logWarning(key, $"can't use {conditionKey} as a config field, because it's a reserved condition name.");
                    continue;
                }

                // read allowed values
                InvariantHashSet allowValues = this.ParseCommaDelimitedField(field.AllowValues);
                if (!allowValues.Any())
                {
                    logWarning(key, $"no {nameof(ConfigSchemaFieldConfig.AllowValues)} specified.");
                    continue;
                }

                // read default values
                InvariantHashSet defaultValues = this.ParseCommaDelimitedField(field.Default);
                {
                    // inject default
                    if (!defaultValues.Any() && !field.AllowBlank)
                    {
                        defaultValues = new InvariantHashSet(allowValues.First());
                    }

                    // validate values
                    string[] invalidValues = defaultValues.Except(allowValues).ToArray();
                    if (invalidValues.Any())
                    {
                        logWarning(key, $"default values '{string.Join(", ", invalidValues)}' are not allowed according to {nameof(ConfigSchemaFieldConfig.AllowBlank)}.");
                        continue;
                    }

                    // validate allow multiple
                    if (!field.AllowMultiple && defaultValues.Count > 1)
                    {
                        logWarning(key, $"can't have multiple default values because {nameof(ConfigSchemaFieldConfig.AllowMultiple)} is false.");
                        continue;
                    }
                }

                // add to schema
                schema[key] = new ConfigField(allowValues, defaultValues, field.AllowBlank, field.AllowMultiple);
            }

            return(schema);
        }
Exemple #4
0
        /****
        ** Condition parsing
        ****/
        /// <summary>Normalise and parse the given condition values.</summary>
        /// <param name="raw">The raw condition values to normalise.</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, 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);
                }

                // 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
                InvariantHashSet validValues = new InvariantHashSet(this.ConditionFactory.GetValidValues(key));
                {
                    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);
        }
Exemple #5
0
        /// <summary>Save the configuration file for a content pack.</summary>
        /// <param name="contentPack">The content pack.</param>
        /// <param name="config">The configuration to save.</param>
        /// <param name="modHelper">The mod helper through which to save the file.</param>
        public void Save(IContentPack contentPack, InvariantDictionary <ConfigField> config, IModHelper modHelper)
        {
            string configPath = Path.Combine(contentPack.DirectoryPath, this.Filename);

            // save if settings valid
            if (config.Any())
            {
                InvariantDictionary <string> data = new InvariantDictionary <string>(config.ToDictionary(p => p.Key, p => string.Join(", ", p.Value.Value)));
                modHelper.WriteJsonFile(configPath, data);
            }

            // delete if no settings
            else if (File.Exists(configPath))
            {
                File.Delete(configPath);
            }
        }
Exemple #6
0
        /// <summary>Save the configuration file for a content pack.</summary>
        /// <param name="contentPack">The content pack.</param>
        /// <param name="config">The configuration to save.</param>
        /// <param name="modHelper">The mod helper through which to save the file.</param>
        public void Save(ManagedContentPack contentPack, InvariantDictionary <ConfigField> config, IModHelper modHelper)
        {
            // save if settings valid
            if (config.Any())
            {
                InvariantDictionary <string> data = new InvariantDictionary <string>(config.ToDictionary(p => p.Key, p => string.Join(", ", p.Value.Value)));
                contentPack.WriteJsonFile(this.Filename, data);
            }

            // delete if no settings
            else
            {
                FileInfo file = new FileInfo(Path.Combine(contentPack.GetFullPath(this.Filename)));
                if (file.Exists)
                {
                    file.Delete();
                }
            }
        }
Exemple #7
0
        /****
        ** 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);
        }
Exemple #8
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="migrator">The migrator which validates and migrates content pack data.</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, IMigration migrator, out ConditionDictionary conditions, out string error)
        {
            conditions = new ConditionDictionary();

            // no conditions
            if (raw == null || !raw.Any())
            {
                error = null;
                return(true);
            }

            // parse conditions
            Lexer lexer = new Lexer();

            foreach (KeyValuePair <string, string> pair in raw)
            {
                // parse condition key
                ILexToken[] lexTokens = lexer.ParseBits(pair.Key, impliedBraces: true).ToArray();
                if (lexTokens.Length != 1 || !(lexTokens[0] is LexTokenToken lexToken) || lexToken.PipedTokens.Any())
                {
                    error      = $"'{pair.Key}' isn't a valid token name";
                    conditions = null;
                    return(false);
                }
                TokenName name = new TokenName(lexToken.Name, lexToken.InputArg?.Text);

                // apply migrations
                if (!migrator.TryMigrate(ref name, out error))
                {
                    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);
                    }
                }

                // parse values
                InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value);
                if (!values.Any())
                {
                    error      = $"{name} can't be empty";
                    conditions = null;
                    return(false);
                }

                // validate token keys & values
                if (!token.TryValidate(name, values, out string customError))
                {
                    error      = $"invalid {name} condition: {customError}";
                    conditions = null;
                    return(false);
                }

                // create condition
                conditions[name] = new Condition(name, values);
            }

            // return parsed conditions
            error = null;
            return(true);
        }
Exemple #9
0
        private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks)
        {
            // load content packs
            ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}"));

            foreach (RawContentPack current in contentPacks)
            {
                this.Monitor.VerboseLog($"Loading content pack '{current.Manifest.Name}'...");

                try
                {
                    ContentConfig content = current.Content;

                    // load tokens
                    ModTokenContext tokenContext = this.TokenManager.TrackLocalTokens(current.ManagedPack.Pack);
                    {
                        // load config.json
                        InvariantDictionary <ConfigField> config = configFileHandler.Read(current.ManagedPack, content.ConfigSchema);
                        configFileHandler.Save(current.ManagedPack, config, this.Helper);
                        if (config.Any())
                        {
                            this.Monitor.VerboseLog($"   found config.json with {config.Count} fields...");
                        }

                        // load config tokens
                        foreach (KeyValuePair <string, ConfigField> pair in config)
                        {
                            ConfigField field = pair.Value;
                            tokenContext.Add(new ImmutableToken(pair.Key, field.Value, allowedValues: field.AllowValues, canHaveMultipleValues: field.AllowMultiple));
                        }

                        // load dynamic tokens
                        foreach (DynamicTokenConfig entry in content.DynamicTokens ?? new DynamicTokenConfig[0])
                        {
                            void LogSkip(string reason) => this.Monitor.Log($"Ignored {current.Manifest.Name} > dynamic token '{entry.Name}': {reason}", LogLevel.Warn);

                            // validate token key
                            if (!TokenName.TryParse(entry.Name, out TokenName name))
                            {
                                LogSkip("the name could not be parsed as a token key.");
                                continue;
                            }
                            if (name.HasSubkey())
                            {
                                LogSkip("the token name cannot contain a subkey (:).");
                                continue;
                            }
                            if (name.TryGetConditionType(out ConditionType conflictingType))
                            {
                                LogSkip($"conflicts with global token '{conflictingType}'.");
                                continue;
                            }
                            if (config.ContainsKey(name.Key))
                            {
                                LogSkip($"conflicts with player config token '{conflictingType}'.");
                                continue;
                            }

                            // parse values
                            InvariantHashSet values = entry.Value != null?this.ParseCommaDelimitedField(entry.Value) : new InvariantHashSet();

                            // parse conditions
                            ConditionDictionary conditions;
                            {
                                if (!this.TryParseConditions(entry.When, tokenContext, current.Migrator, out conditions, out string error))
                                {
                                    this.Monitor.Log($"Ignored {current.Manifest.Name} > '{entry.Name}' token: its {nameof(DynamicTokenConfig.When)} field is invalid: {error}.", LogLevel.Warn);
                                    continue;
                                }
                            }

                            // add token
                            tokenContext.Add(new DynamicTokenValue(name, values, conditions));
                        }
                    }

                    // load patches
                    content.Changes = this.SplitPatches(content.Changes).ToArray();
                    this.NamePatches(current.ManagedPack, content.Changes);
                    foreach (PatchConfig patch in content.Changes)
                    {
                        this.Monitor.VerboseLog($"   loading {patch.LogName}...");
                        this.LoadPatch(current.ManagedPack, patch, tokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn));
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error);
                    continue;
                }
            }
        }
        /*********
        ** 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);
        }
Exemple #11
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="migrator">The migrator which validates and migrates content pack data.</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, IMigration migrator, out IList <Condition> conditions, out string error)
        {
            conditions = new List <Condition>();

            // no conditions
            if (raw == null || !raw.Any())
            {
                error = null;
                return(true);
            }

            // parse conditions
            Lexer lexer = new Lexer();

            foreach (KeyValuePair <string, string> pair in raw)
            {
                // get lexical tokens
                ILexToken[] lexTokens = lexer.ParseBits(pair.Key, impliedBraces: true).ToArray();
                for (int i = 0; i < lexTokens.Length; i++)
                {
                    if (!migrator.TryMigrate(ref lexTokens[0], out error))
                    {
                        conditions = null;
                        return(false);
                    }
                }

                // parse condition key
                if (lexTokens.Length != 1 || !(lexTokens[0] is LexTokenToken lexToken))
                {
                    error      = $"'{pair.Key}' isn't a valid token name";
                    conditions = null;
                    return(false);
                }
                ITokenString input = new TokenString(lexToken.InputArg, tokenContext);

                // get token
                IToken token = tokenContext.GetToken(lexToken.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 input
                if (!token.TryValidateInput(input, out error))
                {
                    conditions = null;
                    return(false);
                }

                // parse values
                if (string.IsNullOrWhiteSpace(pair.Value))
                {
                    error      = $"can't parse condition {pair.Key}: value can't be empty";
                    conditions = null;
                    return(false);
                }
                if (!this.TryParseStringTokens(pair.Value, tokenContext, migrator, out error, out ITokenString values))
                {
                    error = $"can't parse condition {pair.Key}: {error}";
                    return(false);
                }

                // validate token keys & values
                if (!values.IsMutable && !token.TryValidateValues(input, values.SplitValues(), tokenContext, out string customError))
                {
                    error      = $"invalid {lexToken.Name} condition: {customError}";
                    conditions = null;
                    return(false);
                }

                // create condition
                conditions.Add(new Condition(name: token.Name, input: input, values: values));
            }

            // return parsed conditions
            error = null;
            return(true);
        }
Exemple #12
0
        private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks)
        {
            // load content packs
            ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}", LogLevel.Warn));

            foreach (RawContentPack current in contentPacks)
            {
                this.Monitor.VerboseLog($"Loading content pack '{current.Manifest.Name}'...");

                try
                {
                    ContentConfig content = current.Content;

                    // load tokens
                    ModTokenContext modContext = this.TokenManager.TrackLocalTokens(current.ManagedPack.Pack);
                    {
                        // load config.json
                        InvariantDictionary <ConfigField> config = configFileHandler.Read(current.ManagedPack, content.ConfigSchema, current.Content.Format);
                        configFileHandler.Save(current.ManagedPack, config, this.Helper);
                        if (config.Any())
                        {
                            this.Monitor.VerboseLog($"   found config.json with {config.Count} fields...");
                        }

                        // load config tokens
                        foreach (KeyValuePair <string, ConfigField> pair in config)
                        {
                            ConfigField field = pair.Value;
                            modContext.Add(new ImmutableToken(pair.Key, field.Value, scope: current.Manifest.UniqueID, allowedValues: field.AllowValues, canHaveMultipleValues: field.AllowMultiple));
                        }

                        // load dynamic tokens
                        foreach (DynamicTokenConfig entry in content.DynamicTokens ?? new DynamicTokenConfig[0])
                        {
                            void LogSkip(string reason) => this.Monitor.Log($"Ignored {current.Manifest.Name} > dynamic token '{entry.Name}': {reason}", LogLevel.Warn);

                            // validate token key
                            if (string.IsNullOrWhiteSpace(entry.Name))
                            {
                                LogSkip("the token name can't be empty.");
                                continue;
                            }
                            if (entry.Name.Contains(InternalConstants.InputArgSeparator))
                            {
                                LogSkip($"the token name can't have an input argument ({InternalConstants.InputArgSeparator} character).");
                                continue;
                            }
                            if (Enum.TryParse <ConditionType>(entry.Name, true, out _))
                            {
                                LogSkip("the token name is already used by a global token.");
                                continue;
                            }
                            if (config.ContainsKey(entry.Name))
                            {
                                LogSkip("the token name is already used by a config token.");
                                continue;
                            }

                            // parse values
                            ITokenString values;
                            if (!string.IsNullOrWhiteSpace(entry.Value))
                            {
                                if (!this.TryParseStringTokens(entry.Value, modContext, current.Migrator, out string valueError, out values))
                                {
                                    LogSkip($"the token value is invalid: {valueError}");
                                    continue;
                                }
                            }
                            else
                            {
                                values = new LiteralString("");
                            }

                            // parse conditions
                            IList <Condition> conditions;
                            {
                                if (!this.TryParseConditions(entry.When, modContext, current.Migrator, out conditions, out string conditionError))
                                {
                                    this.Monitor.Log($"Ignored {current.Manifest.Name} > '{entry.Name}' token: its {nameof(DynamicTokenConfig.When)} field is invalid: {conditionError}.", LogLevel.Warn);
                                    continue;
                                }
                            }

                            // add token
                            modContext.Add(new DynamicTokenValue(entry.Name, values, conditions));
                        }
                    }

                    // load patches
                    IContext patchTokenContext = new SinglePatchContext(current.Manifest.UniqueID, parentContext: modContext); // make patch tokens available to patches

                    content.Changes = this.SplitPatches(content.Changes).ToArray();
                    this.NamePatches(current.ManagedPack, content.Changes);

                    foreach (PatchConfig patch in content.Changes)
                    {
                        this.Monitor.VerboseLog($"   loading {patch.LogName}...");
                        this.LoadPatch(current.ManagedPack, patch, patchTokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn));
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error);
                    continue;
                }
            }
        }
Exemple #13
0
        /// <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="entry">The change to load.</param>
        /// <param name="config">The content pack's config values.</param>
        /// <param name="logSkip">The callback to invoke with the error reason if loading it fails.</param>
        private bool LoadPatch(IContentPack pack, PatchConfig entry, InvariantDictionary <ConfigField> config, 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, config, out string error, out TokenStringBuilder builder))
                    {
                        return(TrackSkip($"the {nameof(PatchConfig.Target)} is invalid: {error}"));
                    }
                    assetName = builder.Build();
                }

                // parse 'enabled'
                bool enabled = true;
                {
                    if (entry.Enabled != null && !this.TryParseBoolean(entry.Enabled, config, out string error, out enabled))
                    {
                        return(TrackSkip($"invalid {nameof(PatchConfig.Enabled)} value '{entry.Enabled}': {error}"));
                    }
                }

                // apply config
                foreach (string key in config.Keys)
                {
                    if (entry.When.TryGetValue(key, out string values))
                    {
                        InvariantHashSet expected = this.PatchManager.ParseCommaDelimitedField(values);
                        if (!expected.Intersect(config[key].Value).Any())
                        {
                            return(TrackSkip($"disabled: config field '{key}' must have one of '{string.Join(", ", expected)}', but found '{string.Join(", ", config[key].Value)}'.", warn: false));
                        }

                        entry.When.Remove(key);
                    }
                }

                // parse conditions
                ConditionDictionary conditions;
                {
                    if (!this.PatchManager.TryParseConditions(entry.When, 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, config, conditions, out string error, out TokenString fromAsset, checkOnly: !enabled))
                    {
                        return(TrackSkip(error));
                    }
                    patch = new LoadPatch(entry.LogName, this.AssetLoader, pack, assetName, conditions, fromAsset, this.Helper.Content.NormaliseAssetName);

                    // detect conflicting loaders
                    if (enabled)
                    {
                        InvariantDictionary <IPatch> conflicts = this.PatchManager.GetConflictingLoaders(patch);
                        if (conflicts.Any())
                        {
                            IEnumerable <string> conflictNames = (from conflict in conflicts
                                                                  orderby conflict.Key
                                                                  select $"'{conflict.Value.LogName}' already loads {conflict.Key}");
                            return(TrackSkip(
                                       $"{nameof(entry.Target)} '{patch.TokenableAssetName.Raw}' conflicts with other load patches ({string.Join(", ", conflictNames)}). Each file can only be loaded by one patch, unless their conditions can never overlap."));
                        }
                    }
                }
                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 => string.IsNullOrWhiteSpace(p.Value)))
                    {
                        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."));
                    }

                    // save
                    patch = new EditDataPatch(entry.LogName, this.AssetLoader, pack, assetName, conditions, entry.Entries, entry.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, config, conditions, out string error, out TokenString fromAsset, checkOnly: !enabled))
                    {
                        return(TrackSkip(error));
                    }
                    patch = new EditImagePatch(entry.LogName, this.AssetLoader, pack, assetName, conditions, fromAsset, entry.FromArea, entry.ToArea, patchMode, this.Monitor, this.Helper.Content.NormaliseAssetName);
                }
                break;

                default:
                    return(TrackSkip($"unsupported patch type '{action}'."));
                }

                // only apply patch when its tokens are available
                HashSet <ConditionKey> tokensUsed = new HashSet <ConditionKey>(patch.GetTokensUsed());
                foreach (ConditionKey key in tokensUsed)
                {
                    if (!patch.Conditions.ContainsKey(key))
                    {
                        patch.Conditions.Add(key, patch.Conditions.GetValidValues(key));
                    }
                }

                // 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}"));
            }
        }
Exemple #14
0
        private void LoadContentPacks()
        {
            ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.PatchManager.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}"));

            foreach (IContentPack pack in this.Helper.GetContentPacks())
            {
                this.VerboseLog($"Loading content pack '{pack.Manifest.Name}'...");

                try
                {
                    // read changes file
                    ContentConfig content = pack.ReadJsonFile <ContentConfig>(this.PatchFileName);
                    if (content == null)
                    {
                        this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it has no {this.PatchFileName} file.", LogLevel.Error);
                        continue;
                    }
                    if (content.Format == null || content.Changes == null)
                    {
                        this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it doesn't specify the required {nameof(ContentConfig.Format)} or {nameof(ContentConfig.Changes)} fields.", LogLevel.Error);
                        continue;
                    }

                    // validate version
                    if (!this.SupportedFormatVersions.Contains(content.Format.ToString()))
                    {
                        this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it uses unsupported format {content.Format} (supported version: {string.Join(", ", this.SupportedFormatVersions)}).", LogLevel.Error);
                        continue;
                    }

                    // load config.json
                    InvariantDictionary <ConfigField> config = configFileHandler.Read(pack, content.ConfigSchema);
                    configFileHandler.Save(pack, config, this.Helper);
                    if (config.Any())
                    {
                        this.VerboseLog($"   found config.json with {config.Count} fields...");
                    }

                    // validate features
                    if (content.Format.IsOlderThan("1.3"))
                    {
                        if (config.Any())
                        {
                            this.Monitor.Log($"Loading content pack '{pack.Manifest.Name}' failed. It specifies format version {content.Format}, but uses the {nameof(ContentConfig.ConfigSchema)} field added in 1.3.", LogLevel.Error);
                            continue;
                        }
                        if (content.Changes.Any(p => p.FromFile != null && p.FromFile.Contains("{{")))
                        {
                            this.Monitor.Log($"Loading content pack '{pack.Manifest.Name}' failed. It specifies format version {content.Format}, but uses the {{{{token}}}} feature added in 1.3.", LogLevel.Error);
                            continue;
                        }
                        if (content.Changes.Any(p => p.When != null && p.When.Any()))
                        {
                            this.Monitor.Log($"Loading content pack '{pack.Manifest.Name}' failed. It specifies format version {content.Format}, but uses the condition feature ({nameof(ContentConfig.Changes)}.{nameof(PatchConfig.When)} field) added in 1.3.", LogLevel.Error);
                            continue;
                        }
                    }

                    // load patches
                    this.NamePatches(pack, content.Changes);
                    foreach (PatchConfig patch in content.Changes)
                    {
                        this.VerboseLog($"   loading {patch.LogName}...");
                        this.LoadPatch(pack, patch, config, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn));
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Error loading content pack '{pack.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error);
                }
            }
        }
Exemple #15
0
        /****
        ** 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);
        }
Exemple #16
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);
        }
Exemple #17
0
        /*********
        ** 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>
        private InvariantDictionary <ConfigField> LoadConfigSchema(InvariantDictionary <ConfigSchemaFieldConfig> rawSchema, Action <string, string> logWarning)
        {
            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 (!TokenName.TryParse(rawKey, out TokenName name))
                {
                    logWarning(rawKey, $"the name '{rawKey}' is not in a valid format.");
                    continue;
                }
                if (name.HasSubkey())
                {
                    logWarning(rawKey, $"the name '{rawKey}' can't have a subkey (:).");
                    continue;
                }

                // validate reserved keys
                if (name.TryGetConditionType(out ConditionType _))
                {
                    logWarning(rawKey, $"can't use {name.Key} as a config field, because it's a reserved condition key.");
                    continue;
                }

                // read allowed values
                InvariantHashSet allowValues = this.ParseCommaDelimitedField(field.AllowValues);
                if (!allowValues.Any())
                {
                    logWarning(rawKey, $"no {nameof(ConfigSchemaFieldConfig.AllowValues)} specified.");
                    continue;
                }

                // read default values
                InvariantHashSet defaultValues = this.ParseCommaDelimitedField(field.Default);
                {
                    // inject default
                    if (!defaultValues.Any() && !field.AllowBlank)
                    {
                        defaultValues = new InvariantHashSet(allowValues.First());
                    }

                    // validate values
                    string[] invalidValues = defaultValues.ExceptIgnoreCase(allowValues).ToArray();
                    if (invalidValues.Any())
                    {
                        logWarning(rawKey, $"default values '{string.Join(", ", invalidValues)}' are not allowed according to {nameof(ConfigSchemaFieldConfig.AllowBlank)}.");
                        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);
        }