コード例 #1
0
        /// <summary>Perform custom validation.</summary>
        /// <param name="name">The token name to validate.</param>
        /// <param name="values">The values to validate.</param>
        /// <param name="error">The validation error, if any.</param>
        /// <returns>Returns whether validation succeeded.</returns>
        public bool TryValidate(TokenName name, InvariantHashSet values, out string error)
        {
            // parse data
            KeyValuePair <TokenName, string>[] pairs = this.GetSubkeyValuePairsFor(name, values).ToArray();

            // restrict to allowed subkeys
            if (this.CanHaveSubkeys)
            {
                InvariantHashSet validKeys = this.GetAllowedSubkeys();
                if (validKeys?.Any() == true)
                {
                    string[] invalidSubkeys =
                        (
                            from pair in pairs
                            where pair.Key.Subkey != null && !validKeys.Contains(pair.Key.Subkey)
                            select pair.Key.Subkey
                        )
                        .Distinct()
                        .ToArray();
                    if (invalidSubkeys.Any())
                    {
                        error = $"invalid subkeys ({string.Join(", ", invalidSubkeys)}); expected one of {string.Join(", ", validKeys)}";
                        return(false);
                    }
                }
            }

            // restrict to allowed values
            {
                InvariantHashSet validValues = this.GetAllowedValues(name);
                if (validValues?.Any() == true)
                {
                    string[] invalidValues =
                        (
                            from pair in pairs
                            where !validValues.Contains(pair.Value)
                            select pair.Value
                        )
                        .Distinct()
                        .ToArray();
                    if (invalidValues.Any())
                    {
                        error = $"invalid values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}";
                        return(false);
                    }
                }
            }

            // custom validation
            foreach (KeyValuePair <TokenName, string> pair in pairs)
            {
                if (!this.Values.TryValidate(pair.Key.Subkey, new InvariantHashSet {
                    pair.Value
                }, out error))
                {
                    return(false);
                }
            }

            // no issues found
            error = null;
            return(true);
        }
コード例 #2
0
 /// <inheritdoc />
 public override bool HasBoundedValues(IInputArguments input, out InvariantHashSet allowedValues)
 {
     allowedValues = this.AllowedRootValues;
     return(allowedValues != null);
 }
コード例 #3
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);
        }
コード例 #4
0
 /// <summary>Set the current values.</summary>
 /// <param name="values">The values to set.</param>
 public void SetValue(ITokenString values)
 {
     this.Values = values.SplitValues();
 }
コード例 #5
0
ファイル: TokenParser.cs プロジェクト: corrinr/StardewMods-1
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="tokenContext">The tokens available for this content pack.</param>
 /// <param name="forMod">The manifest for the content pack being parsed.</param>
 /// <param name="migrator">The migrator which validates and migrates content pack data.</param>
 /// <param name="installedMods">The mod IDs which are currently installed.</param>
 public TokenParser(IContext tokenContext, IManifest forMod, IMigration migrator, InvariantHashSet installedMods)
 {
     this.Context       = tokenContext;
     this.ForMod        = forMod;
     this.Migrator      = migrator;
     this.InstalledMods = installedMods;
 }
コード例 #6
0
 /// <inheritdoc />
 public override bool HasBoundedValues(IInputArguments input, out InvariantHashSet allowedValues)
 {
     allowedValues = InvariantHashSet.Boolean();
     return(true);
 }
コード例 #7
0
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="key">The name of the token whose value to set.</param>
 /// <param name="value">The token value to set.</param>
 /// <param name="conditions">The conditions that must match to set this value.</param>
 public DynamicTokenValue(TokenName key, InvariantHashSet value, ConditionDictionary conditions)
 {
     this.Name       = key;
     this.Value      = value;
     this.Conditions = conditions;
 }
コード例 #8
0
        /// <summary>Parse a user-defined set of values for input/value pairs. For example, <c>"Abigail:10"</c> for a relationship token would be parsed as input argument 'Abigail' with value '10'.</summary>
        /// <param name="input">The current input argument, if applicable.</param>
        /// <param name="values">The values to parse.</param>
        /// <returns>Returns the input/value pairs found. If <paramref name="input"/> is non-null, the <paramref name="values"/> are treated as values for that input argument. Otherwise if <see cref="AllowsInput"/> is true, then each value is treated as <c>input:value</c> (if they contain a colon) or <c>value</c> (with a null input).</returns>
        protected IEnumerable <KeyValuePair <string, string> > GetInputValuePairs(string input, InvariantHashSet values)
        {
            // no input arguments in values
            if (!this.AllowsInput || input != null)
            {
                foreach (string value in values)
                {
                    yield return(new KeyValuePair <string, string>(input, value));
                }
            }

            // possible input arguments in values
            else
            {
                foreach (string value in values)
                {
                    string[] parts = value.Split(new[] { ':' }, 2);
                    if (parts.Length < 2)
                    {
                        yield return(new KeyValuePair <string, string>(input, parts[0]));
                    }
                    else
                    {
                        yield return(new KeyValuePair <string, string>(parts[0], parts[1]));
                    }
                }
            }
        }
コード例 #9
0
        /// <summary>Validate that the provided values are valid for the input argument (regardless of whether they match).</summary>
        /// <param name="input">The input argument, if applicable.</param>
        /// <param name="values">The values to validate.</param>
        /// <param name="error">The validation error, if any.</param>
        /// <returns>Returns whether validation succeeded.</returns>
        public bool TryValidate(string input, InvariantHashSet values, out string error)
        {
            // parse data
            KeyValuePair <string, string>[] pairs = this.GetInputValuePairs(input, values).ToArray();

            // restrict to allowed input
            if (this.AllowsInput)
            {
                InvariantHashSet validInputs = this.GetValidInputs();
                if (validInputs?.Any() == true)
                {
                    string[] invalidInputs =
                        (
                            from pair in pairs
                            where pair.Key != null && !validInputs.Contains(pair.Key)
                            select pair.Key
                        )
                        .Distinct()
                        .ToArray();
                    if (invalidInputs.Any())
                    {
                        error = $"invalid input arguments ({string.Join(", ", invalidInputs)}), expected any of {string.Join(", ", validInputs)}";
                        return(false);
                    }
                }
            }

            // restrict to allowed values
            {
                InvariantHashSet validValues = this.GetAllowedValues(input);
                if (validValues?.Any() == true)
                {
                    string[] invalidValues =
                        (
                            from pair in pairs
                            where !validValues.Contains(pair.Value)
                            select pair.Value
                        )
                        .Distinct()
                        .ToArray();
                    if (invalidValues.Any())
                    {
                        error = $"invalid values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}";
                        return(false);
                    }
                }
            }

            // custom validation
            foreach (KeyValuePair <string, string> pair in pairs)
            {
                if (!this.TryValidate(pair.Key, pair.Value, out error))
                {
                    return(false);
                }
            }

            // no issues found
            error = null;
            return(true);
        }
コード例 #10
0
 /// <summary>Get the allowed values for a token name (or <c>null</c> if any value is allowed).</summary>
 /// <exception cref="InvalidOperationException">The key doesn't match this token, or the key does not respect <see cref="IToken.CanHaveSubkeys"/> or <see cref="IToken.RequiresSubkeys"/>.</exception>
 public override InvariantHashSet GetAllowedValues(TokenName name)
 {
     return(name.HasSubkey()
         ? InvariantHashSet.Boolean()
         : this.AllowedRootValues);
 }
コード例 #11
0
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="customAttachments">The enabled custom tool or item names.</param>
 public CustomAttachment(string[] customAttachments)
 {
     this.CustomNames = new InvariantHashSet(customAttachments);
 }
コード例 #12
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 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.Except(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);
        }
コード例 #13
0
 /// <summary>Construct an instance.</summary>
 /// <param name="name">The token name.</param>
 /// <param name="canHaveMultipleValues">Whether the token may contain multiple values (or <c>null</c> to set it based on the given values).</param>
 /// <param name="allowedValues">The allowed values (or <c>null</c> if any value is allowed).</param>
 /// <param name="values">Get the current token values.</param>
 public ImmutableToken(string name, InvariantHashSet values, InvariantHashSet allowedValues = null, bool?canHaveMultipleValues = null)
     : this(TokenName.Parse(name), values, allowedValues, canHaveMultipleValues)
 {
 }
コード例 #14
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>
        /// <param name="formatVersion">The content format version.</param>
        private InvariantDictionary <ConfigField> LoadConfigSchema(InvariantDictionary <ConfigSchemaFieldConfig> rawSchema, Action <string, string> logWarning, ISemanticVersion formatVersion)
        {
            InvariantDictionary <ConfigField> schema = new InvariantDictionary <ConfigField>();

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

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

                // validate format
                if (string.IsNullOrWhiteSpace(rawKey))
                {
                    logWarning(rawKey, "the config field name can't be empty.");
                    continue;
                }
                if (rawKey.Contains(InternalConstants.PositionalInputArgSeparator) || rawKey.Contains(InternalConstants.NamedInputArgSeparator))
                {
                    logWarning(rawKey, $"the name '{rawKey}' can't have input arguments ({InternalConstants.PositionalInputArgSeparator} or {InternalConstants.NamedInputArgSeparator} character).");
                    continue;
                }

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

                // read allowed/default values
                InvariantHashSet allowValues   = this.ParseCommaDelimitedField(field.AllowValues);
                InvariantHashSet defaultValues = this.ParseCommaDelimitedField(field.Default);

                // pre-1.7 behaviour
                if (formatVersion.IsOlderThan("1.7"))
                {
                    // allowed values are required
                    if (!allowValues.Any())
                    {
                        logWarning(rawKey, $"no {nameof(ConfigSchemaFieldConfig.AllowValues)} specified (and format version is less than 1.7).");
                        continue;
                    }

                    // inject default if needed
                    if (!defaultValues.Any() && !field.AllowBlank)
                    {
                        defaultValues = new InvariantHashSet(allowValues.First());
                    }
                }

                // validate allowed values
                if (!field.AllowBlank && !defaultValues.Any())
                {
                    logWarning(rawKey, $"if {nameof(field.AllowBlank)} is false, you must specify {nameof(field.Default)}.");
                    continue;
                }
                if (allowValues.Any() && defaultValues.Any())
                {
                    string[] invalidValues = defaultValues.ExceptIgnoreCase(allowValues).ToArray();
                    if (invalidValues.Any())
                    {
                        logWarning(rawKey, $"default values '{string.Join(", ", invalidValues)}' are not allowed according to {nameof(ConfigSchemaFieldConfig.AllowValues)}.");
                        continue;
                    }
                }

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

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

            return(schema);
        }
コード例 #15
0
        /// <inheritdoc />
        public virtual bool TryValidateInput(IInputArguments input, out string error)
        {
            if (input.IsReady)
            {
                // validate positional arguments
                if (input.HasPositionalArgs)
                {
                    // check if input allowed
                    if (!this.AllowsPositionalInput)
                    {
                        error = $"invalid input arguments ({input.TokenString}), token {this.Name} doesn't allow input.";
                        return(false);
                    }

                    // check argument count
                    if (input.PositionalArgs.Length > this.MaxPositionalArgs)
                    {
                        error = $"invalid input arguments ({input.TokenString}), token {this.Name} doesn't allow more than {this.MaxPositionalArgs} argument{(this.MaxPositionalArgs == 1 ? "" : "s")}.";
                        return(false);
                    }

                    // check values
                    if (input.TokenString.Value != InternalConstants.TokenPlaceholder)
                    {
                        InvariantHashSet validInputs = this.GetValidPositionalArgs();
                        if (validInputs?.Any() == true)
                        {
                            if (input.PositionalArgs.Any(arg => !validInputs.Contains(arg)))
                            {
                                string raw    = input.TokenString.Raw;
                                string parsed = input.TokenString.Value;
                                error = $"invalid input arguments ({(raw != parsed ? $"{raw} => {parsed}" : parsed)}) for {this.Name} token, expected any of '{string.Join("', '", validInputs.OrderByIgnoreCase(p => p))}'";
                                return(false);
                            }
                        }
                    }
                }

                // validate named arguments
                if (input.HasNamedArgs)
                {
                    if (this.ValidNamedArguments != null)
                    {
                        if (!this.ValidNamedArguments.Any())
                        {
                            error = $"invalid named argument '{input.NamedArgs.First().Key}' for {this.Name} token, which does not accept any named arguments.";
                            return(false);
                        }

                        string invalidKey = (from arg in input.NamedArgs where !this.ValidNamedArguments.Contains(arg.Key) select arg.Key).FirstOrDefault();
                        if (invalidKey != null)
                        {
                            error = $"invalid named argument '{invalidKey}' for {this.Name} token, expected any of '{string.Join("', '", this.ValidNamedArguments.OrderByIgnoreCase(p => p))}'";
                            return(false);
                        }
                    }
                }
            }

            // no issues found
            error = null;
            return(true);
        }
コード例 #16
0
 /// <summary>Get whether the token always chooses from a set of known values for the given input. Mutually exclusive with <see cref="IToken.HasBoundedRangeValues"/>.</summary>
 /// <param name="input">The input argument, if applicable.</param>
 /// <param name="allowedValues">The possible values for the input.</param>
 /// <exception cref="InvalidOperationException">The input argument doesn't match this value provider, or does not respect <see cref="IToken.CanHaveInput"/> or <see cref="IToken.RequiresInput"/>.</exception>
 public virtual bool HasBoundedValues(ITokenString input, out InvariantHashSet allowedValues)
 {
     return(this.Values.HasBoundedValues(input, out allowedValues));
 }
コード例 #17
0
ファイル: ModEntry.cs プロジェクト: Felix-Dev/StardewMods-1
        /// <summary>Normalise and parse the given condition values.</summary>
        /// <param name="raw">The raw condition values to normalise.</param>
        /// <param name="tokenContext">The tokens available for this content pack.</param>
        /// <param name="formatVersion">The format version specified by the content pack.</param>
        /// <param name="latestFormatVersion">The latest format version.</param>
        /// <param name="minumumTokenVersions">The minimum format versions for newer condition types.</param>
        /// <param name="conditions">The normalised conditions.</param>
        /// <param name="error">An error message indicating why normalisation failed.</param>
        private bool TryParseConditions(InvariantDictionary <string> raw, IContext tokenContext, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, InvariantDictionary <ISemanticVersion> minumumTokenVersions, out ConditionDictionary conditions, out string error)
        {
            conditions = new ConditionDictionary();

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

            // parse conditions
            foreach (KeyValuePair <string, string> pair in raw)
            {
                // parse condition key
                if (!TokenName.TryParse(pair.Key, out TokenName name))
                {
                    error      = $"'{pair.Key}' isn't a valid token name";
                    conditions = null;
                    return(false);
                }

                // get token
                IToken token = tokenContext.GetToken(name, enforceContext: false);
                if (token == null)
                {
                    error      = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", tokenContext.GetTokens(enforceContext: false).Select(p => p.Name).OrderBy(p => p))}";
                    conditions = null;
                    return(false);
                }

                // validate subkeys
                if (!token.CanHaveSubkeys)
                {
                    if (name.HasSubkey())
                    {
                        error      = $"{name.Key} conditions don't allow subkeys (:)";
                        conditions = null;
                        return(false);
                    }
                }
                else if (token.RequiresSubkeys)
                {
                    if (!name.HasSubkey())
                    {
                        error      = $"{name.Key} conditions must specify a token subkey (see readme for usage)";
                        conditions = null;
                        return(false);
                    }
                }

                // check compatibility
                if (minumumTokenVersions.TryGetValue(name.Key, out ISemanticVersion minVersion) && minVersion.IsNewerThan(formatVersion))
                {
                    error      = $"{name} isn't available with format version {formatVersion} (change the {nameof(ContentConfig.Format)} field to {latestFormatVersion} to use newer features)";
                    conditions = null;
                    return(false);
                }

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

                // restrict to allowed values
                InvariantHashSet rawValidValues = token.GetAllowedValues(name);
                if (rawValidValues?.Any() == true)
                {
                    InvariantHashSet validValues = new InvariantHashSet(rawValidValues);
                    {
                        string[] invalidValues = values.ExceptIgnoreCase(validValues).ToArray();
                        if (invalidValues.Any())
                        {
                            error      = $"invalid {name} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}";
                            conditions = null;
                            return(false);
                        }
                    }
                }

                // perform custom validation
                if (!token.TryCustomValidation(values, out string customError))
                {
                    error      = $"invalid {name} values: {customError}";
                    conditions = null;
                    return(false);
                }

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

            // return parsed conditions
            error = null;
            return(true);
        }
コード例 #18
0
ファイル: Condition.cs プロジェクト: pedronc1/StardewMods
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="key">The condition key in the context.</param>
 /// <param name="values">The condition values for which this condition is valid.</param>
 public Condition(ConditionKey key, InvariantHashSet values)
 {
     this.Key    = key;
     this.Values = values;
 }
コード例 #19
0
ファイル: CommandHandler.cs プロジェクト: hninsin/StardewMods
        /// <summary>Handle the 'patch summary' command.</summary>
        /// <param name="args">The subcommand arguments.</param>
        /// <returns>Returns whether the command was handled.</returns>
        private bool HandleSummary(string[] args)
        {
            StringBuilder  output = new StringBuilder();
            LogPathBuilder path   = new LogPathBuilder("console command");

            // parse arguments
            bool showFull  = false;
            var  forModIds = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            foreach (string arg in args)
            {
                // flags
                if (arg.Equals("full", StringComparison.OrdinalIgnoreCase))
                {
                    showFull = true;
                    continue;
                }

                // for mod ID
                forModIds.Add(arg);
            }

            // truncate token values if needed
            string GetTruncatedTokenValues(IEnumerable <string> values)
            {
                const int    maxLength       = 200;
                const string truncatedSuffix = "... (use `patch summary full` to see other values)";

                string valueStr = string.Join(", ", values);

                return(showFull || valueStr.Length <= maxLength
                    ? valueStr
                    : $"{valueStr.Substring(0, maxLength - truncatedSuffix.Length)}{truncatedSuffix}");
            }

            // add condition summary
            output.AppendLine();
            output.AppendLine("=====================");
            output.AppendLine("==  Global tokens  ==");
            output.AppendLine("=====================");
            {
                // get data
                var tokensByProvider =
                    (
                        from token in this.TokenManager.GetTokens(enforceContext: false)
                        let inputArgs = token.GetAllowedInputArguments().ToArray()
                                        let rootValues = !token.RequiresInput ? token.GetValues(InputArguments.Empty).ToArray() : new string[0]
                                                         let isMultiValue =
                            inputArgs.Length > 1 ||
                            rootValues.Length > 1 ||
                            (inputArgs.Length == 1 && token.GetValues(new InputArguments(new LiteralString(inputArgs[0], path.With(token.Name, "input")))).Count() > 1)
                            let mod = (token as ModProvidedToken)?.Mod
                                      orderby isMultiValue, token.Name // single-value tokens first, then alphabetically
                        select new { Mod = mod, Token = token }
                    )
                    .GroupBy(p => p.Mod?.Name?.Trim())
                    .OrderBy(p => p.Key) // default tokens (key is null), then tokens added by other mods
                    .ToArray();
                int labelWidth = Math.Max(tokensByProvider.Max(group => group.Max(p => p.Token.Name.Length)), "token name".Length);

                // group by provider mod (if any)
                foreach (var tokenGroup in tokensByProvider)
                {
                    if (tokenGroup.Key != null && forModIds.Any() && !forModIds.Contains(tokenGroup.First().Mod.UniqueID))
                    {
                        continue;
                    }

                    // print mod name
                    output.AppendLine($"   {tokenGroup.Key ?? "Content Patcher"}:");
                    output.AppendLine();

                    // print table header
                    output.AppendLine($"      {"token name".PadRight(labelWidth)} | value");
                    output.AppendLine($"      {"".PadRight(labelWidth, '-')} | -----");

                    // print tokens
                    foreach (IToken token in tokenGroup.Select(p => p.Token))
                    {
                        output.Append($"      {token.Name.PadRight(labelWidth)} | ");

                        if (!token.IsReady)
                        {
                            output.AppendLine("[ ] n/a");
                        }
                        else if (token.RequiresInput)
                        {
                            InvariantHashSet allowedInputs = token.GetAllowedInputArguments();
                            if (allowedInputs.Any())
                            {
                                bool isFirst = true;
                                foreach (string input in allowedInputs.OrderByIgnoreCase(input => input))
                                {
                                    if (isFirst)
                                    {
                                        output.Append("[X] ");
                                        isFirst = false;
                                    }
                                    else
                                    {
                                        output.Append($"      {"".PadRight(labelWidth, ' ')} |     ");
                                    }

                                    output.AppendLine($":{input}: {GetTruncatedTokenValues(token.GetValues(new InputArguments(new LiteralString(input, path.With(token.Name, "input")))))}");
                                }
                            }
                            else
                            {
                                output.AppendLine("[X] (token returns a dynamic value)");
                            }
                        }
                        else
                        {
                            output.AppendLine("[X] " + GetTruncatedTokenValues(token.GetValues(InputArguments.Empty).OrderByIgnoreCase(p => p)));
                        }
                    }

                    output.AppendLine();
                }
            }

            // list custom locations
            {
                var locations = this.CustomLocationLoader.GetCustomLocationData()
                                .Where(p => !forModIds.Any() || forModIds.Contains(p.ModId))
                                .GroupByIgnoreCase(p => p.ModName)
                                .OrderByIgnoreCase(p => p.Key)
                                .ToArray();

                if (locations.Any())
                {
                    output.AppendLine(
                        "======================\n"
                        + "== Custom locations ==\n"
                        + "======================\n"
                        + "The following custom locations were created by content packs.\n"
                        + (forModIds.Any() ? $"\n(Filtered to content pack ID{(forModIds.Count > 1 ? "s" : "")}: {string.Join(", ", forModIds.OrderByIgnoreCase(p => p))}.)\n" : "")
                        );

                    foreach (var locationGroup in locations)
                    {
                        int nameWidth = Math.Max("location name".Length, locationGroup.Max(p => p.Name.Length));

                        output.AppendLine($"{locationGroup.Key}:");
                        output.AppendLine("".PadRight(locationGroup.Key.Length + 1, '-'));
                        output.AppendLine();
                        output.AppendLine($"   {"location name".PadRight(nameWidth)}  | status");
                        output.AppendLine($"   {"".PadRight(nameWidth, '-')}  | ------");
                        foreach (CustomLocationData location in locationGroup.OrderByIgnoreCase(p => p.Name))
                        {
                            output.AppendLine($"   {location.Name.PadRight(nameWidth)}  | {(location.IsEnabled ? "[X] ok" : $"[ ] error: {location.Error}")}");
                        }
                        output.AppendLine();
                    }
                }

                output.AppendLine();
            }


            // list patches
            {
                var patches = this.GetAllPatches()
                              .Where(p => !forModIds.Any() || forModIds.Contains(p.ContentPack.Manifest.UniqueID))
                              .GroupByIgnoreCase(p => p.ContentPack.Manifest.Name)
                              .OrderByIgnoreCase(p => p.Key)
                              .ToArray();

                output.AppendLine(
                    "=====================\n"
                    + "== Content patches ==\n"
                    + "=====================\n"
                    + "The following patches were loaded. For each patch:\n"
                    + "  - 'loaded' shows whether the patch is loaded and enabled (see details for the reason if not).\n"
                    + "  - 'conditions' shows whether the patch matches with the current conditions (see details for the reason if not). If this is unexpectedly false, check (a) the conditions above and (b) your Where field.\n"
                    + "  - 'applied' shows whether the target asset was loaded and patched. If you expected it to be loaded by this point but it's false, double-check (a) that the game has actually loaded the asset yet, and (b) your Targets field is correct.\n"
                    + (forModIds.Any() ? $"\n(Filtered to content pack ID{(forModIds.Count > 1 ? "s" : "")}: {string.Join(", ", forModIds.OrderByIgnoreCase(p => p))}.)\n" : "")
                    + "\n"
                    );
                foreach (var patchGroup in patches)
                {
                    ModTokenContext tokenContext = this.TokenManager.TrackLocalTokens(patchGroup.First().ContentPack);
                    output.AppendLine($"{patchGroup.Key}:");
                    output.AppendLine("".PadRight(patchGroup.Key.Length + 1, '-'));

                    // print tokens
                    {
                        var tokens =
                            (
                                // get non-global tokens
                                from IToken token in tokenContext.GetTokens(enforceContext: false)
                                where token.Scope != null

                                // get input arguments
                                let validInputs = token.IsReady && token.RequiresInput
                                        ? token.GetAllowedInputArguments().Select(p => new LiteralString(p, path.With(patchGroup.Key, token.Name, $"input '{p}'"))).AsEnumerable <ITokenString>()
                                        : new ITokenString[] { null }
                                from ITokenString input in validInputs

                                where !token.RequiresInput || validInputs.Any() // don't show tokens which can't be represented

                                // select display data
                                let result = new
                        {
                            Name = token.RequiresInput ? $"{token.Name}:{input}" : token.Name,
                            Values = token.IsReady ? token.GetValues(new InputArguments(input)).ToArray() : new string[0],
                            IsReady = token.IsReady
                        }
                                orderby result.Name
                                select result
                            )
                            .ToArray();
                        if (tokens.Any())
                        {
                            int labelWidth = Math.Max(tokens.Max(p => p.Name.Length), "token name".Length);

                            output.AppendLine();
                            output.AppendLine("   Local tokens:");

                            output.AppendLine($"      {"token name".PadRight(labelWidth)} | value");
                            output.AppendLine($"      {"".PadRight(labelWidth, '-')} | -----");

                            foreach (var token in tokens)
                            {
                                output.AppendLine($"      {token.Name.PadRight(labelWidth)} | [{(token.IsReady ? "X" : " ")}] {GetTruncatedTokenValues(token.Values)}");
                            }
                        }
                    }

                    // print patches
                    output.AppendLine();
                    output.AppendLine("   Patches:");
                    output.AppendLine("      loaded  | conditions | applied | name + details");
                    output.AppendLine("      ------- | ---------- | ------- | --------------");
                    foreach (PatchInfo patch in patchGroup.OrderBy(p => p, new PatchDisplaySortComparer()))
                    {
                        // log checkbox and patch name
                        output.Append($"      [{(patch.IsLoaded ? "X" : " ")}]     | [{(patch.MatchesContext ? "X" : " ")}]        | [{(patch.IsApplied ? "X" : " ")}]     | {patch.PathWithoutContentPackPrefix}");

                        // log target value if different from name
                        {
                            // get patch values
                            string rawIdentifyingPath = PathUtilities.NormalizePath(patch.ParsedType == PatchType.Include
                                ? patch.RawFromAsset
                                : patch.RawTargetAsset
                                                                                    );
                            ITokenString parsedIdentifyingPath = patch.ParsedType == PatchType.Include
                                ? patch.ParsedFromAsset
                                : patch.ParsedTargetAsset;

                            // get raw name if different
                            // (ignore differences in whitespace, capitalization, and path separators)
                            string rawValue = !PathUtilities.NormalizePath(patch.PathWithoutContentPackPrefix.ToString().Replace(" ", "")).ContainsIgnoreCase(rawIdentifyingPath?.Replace(" ", ""))
                                ? $"{patch.ParsedType?.ToString() ?? patch.RawType} {rawIdentifyingPath}"
                                : null;

                            // get parsed value
                            string parsedValue = patch.MatchesContext && parsedIdentifyingPath?.HasAnyTokens == true
                                ? PathUtilities.NormalizePath(parsedIdentifyingPath.Value)
                                : null;

                            // format
                            if (rawValue != null || parsedValue != null)
                            {
                                output.Append(" (");
                                if (rawValue != null)
                                {
                                    output.Append(rawValue);
                                    if (parsedValue != null)
                                    {
                                        output.Append(" ");
                                    }
                                }
                                if (parsedValue != null)
                                {
                                    output.Append($"=> {parsedValue}");
                                }
                                output.Append(")");
                            }
                        }

                        // log reason not applied
                        string errorReason = this.GetReasonNotLoaded(patch);
                        if (errorReason != null)
                        {
                            output.Append($"  // {errorReason}");
                        }

                        // log common issues if not applied
                        if (errorReason == null && patch.IsLoaded && !patch.IsApplied && patch.ParsedTargetAsset.IsMeaningful())
                        {
                            string assetName = patch.ParsedTargetAsset.Value;

                            List <string> issues = new List <string>();
                            if (this.AssetNameWithContentPattern.IsMatch(assetName))
                            {
                                issues.Add("shouldn't include 'Content/' prefix");
                            }
                            if (this.AssetNameWithExtensionPattern.IsMatch(assetName))
                            {
                                Match match = this.AssetNameWithExtensionPattern.Match(assetName);
                                issues.Add($"shouldn't include '{match.Captures[0]}' extension");
                            }
                            if (this.AssetNameWithLocalePattern.IsMatch(assetName))
                            {
                                issues.Add("shouldn't include language code (use conditions instead)");
                            }

                            if (issues.Any())
                            {
                                output.Append($" // hint: asset name may be incorrect ({string.Join("; ", issues)}).");
                            }
                        }

                        // log update rate issues
                        if (patch.Patch != null)
                        {
                            foreach (var pair in this.TokenManager.TokensWithSpecialUpdateRates)
                            {
                                UpdateRate       rate       = pair.Item1;
                                string           label      = pair.Item2;
                                InvariantHashSet tokenNames = pair.Item3;

                                if (!patch.Patch.UpdateRate.HasFlag(rate))
                                {
                                    var tokensUsed = new InvariantHashSet(patch.Patch.GetTokensUsed());

                                    string[] locationTokensUsed = tokenNames.Where(p => tokensUsed.Contains(p)).ToArray();
                                    if (locationTokensUsed.Any())
                                    {
                                        output.Append($" // hint: patch uses {label}, but doesn't set \"{nameof(PatchConfig.Update)}\": \"{rate}\".");
                                    }
                                }
                            }
                        }

                        // end line
                        output.AppendLine();
                    }

                    // print patch effects
                    {
                        IDictionary <string, InvariantHashSet> effectsByPatch = new Dictionary <string, InvariantHashSet>(StringComparer.OrdinalIgnoreCase);
                        foreach (PatchInfo patch in patchGroup)
                        {
                            if (!patch.IsApplied || patch.Patch == null)
                            {
                                continue;
                            }

                            string[] changeLabels = patch.GetChangeLabels().ToArray();
                            if (!changeLabels.Any())
                            {
                                continue;
                            }

                            if (!effectsByPatch.TryGetValue(patch.ParsedTargetAsset.Value, out InvariantHashSet effects))
                            {
                                effectsByPatch[patch.ParsedTargetAsset.Value] = effects = new InvariantHashSet();
                            }

                            effects.AddMany(patch.GetChangeLabels());
                        }

                        output.AppendLine();
                        if (effectsByPatch.Any())
                        {
                            int maxAssetNameWidth = Math.Max("asset name".Length, effectsByPatch.Max(p => p.Key.Length));

                            output.AppendLine("   Current changes:");
                            output.AppendLine($"      asset name{"".PadRight(maxAssetNameWidth - "asset name".Length)} | changes");
                            output.AppendLine($"      ----------{"".PadRight(maxAssetNameWidth - "----------".Length, '-')} | -------");

                            foreach (var pair in effectsByPatch.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
                            {
                                output.AppendLine($"      {pair.Key}{"".PadRight(maxAssetNameWidth - pair.Key.Length)} | {string.Join("; ", pair.Value.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}");
                            }
                        }
                        else
                        {
                            output.AppendLine("   No current changes.");
                        }
                    }

                    // add blank line between groups
                    output.AppendLine();
                }
            }

            this.Monitor.Log(output.ToString(), LogLevel.Debug);
            return(true);
        }
コード例 #20
0
        public int Add_Count(params string[] values)
        {
            InvariantHashSet set = new InvariantHashSet(values);

            return(set.Count);
        }
コード例 #21
0
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="name">The value provider name.</param>
 public DynamicTokenValueProvider(string name)
     : base(name, canHaveMultipleValuesForRoot: false)
 {
     this.AllowedRootValues = new InvariantHashSet();
     this.EnableInputArguments(required: false, canHaveMultipleValues: false);
 }
コード例 #22
0
        public bool Contains(string search, params string[] values)
        {
            InvariantHashSet set = new InvariantHashSet(values);

            return(set.Contains(search));
        }
コード例 #23
0
 /// <summary>Get the allowed values for an input argument (or <c>null</c> if any value is allowed).</summary>
 /// <param name="input">The input argument, if applicable.</param>
 /// <exception cref="InvalidOperationException">The input argument doesn't match this value provider, or does not respect <see cref="IValueProvider.AllowsInput"/> or <see cref="IValueProvider.RequiresInput"/>.</exception>
 public override InvariantHashSet GetAllowedValues(ITokenString input)
 {
     return(input.IsMeaningful()
         ? InvariantHashSet.Boolean()
         : this.AllowedRootValues);
 }
コード例 #24
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="contentConfig">The content pack's config.</param>
        /// <param name="entry">The change to load.</param>
        /// <param name="config">The content pack's config values.</param>
        /// <param name="latestFormatVersion">The latest format version.</param>
        /// <param name="logSkip">The callback to invoke with the error reason if loading it fails.</param>
        private bool LoadPatch(ManagedContentPack pack, ContentConfig contentConfig, PatchConfig entry, InvariantDictionary <ConfigField> config, ISemanticVersion latestFormatVersion, 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, contentConfig.Format, latestFormatVersion, out conditions, out string error))
                    {
                        return(TrackSkip($"the {nameof(PatchConfig.When)} field is invalid: {error}."));
                    }
                }

                // validate conditions
                if (action == PatchType.Load)
                {
                    ConditionKey[] tokenisable = this.ConditionFactory.GetTokenisableConditions().ToArray();
                    ConditionKey[] invalid     = conditions.Keys.Except(tokenisable).ToArray();
                    if (invalid.Any())
                    {
                        return(TrackSkip($"disabled: can't use these conditions with {nameof(PatchConfig.Action)} {PatchType.Load}: [{string.Join(", ", invalid)}]"));
                    }
                }

                // 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, shouldPreload: true))
                    {
                        return(TrackSkip(error));
                    }
                    patch = new LoadPatch(entry.LogName, 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 => p.Value != null && p.Value.Trim() == ""))
                    {
                        return(TrackSkip($"the {nameof(PatchConfig.Entries)} can't contain empty values."));
                    }
                    if (entry.Fields != null && entry.Fields.Any(p => p.Value == null || p.Value.Any(n => n.Value == null)))
                    {
                        return(TrackSkip($"the {nameof(PatchConfig.Fields)} can't contain empty values."));
                    }

                    // save
                    patch = new EditDataPatch(entry.LogName, 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, shouldPreload: true))
                    {
                        return(TrackSkip(error));
                    }
                    patch = new EditImagePatch(entry.LogName, pack, assetName, conditions, fromAsset, entry.FromArea, entry.ToArea, patchMode, this.Monitor, this.Helper.Content.NormaliseAssetName);
                }
                break;

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

                // 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}"));
            }
        }
コード例 #25
0
        /*********
        ** Private methods
        *********/
        /// <summary>Get the global value providers with which to initialize the token manager.</summary>
        /// <param name="contentHelper">The content helper from which to load data assets.</param>
        /// <param name="installedMods">The installed mod IDs.</param>
        private IEnumerable <IValueProvider> GetGlobalValueProviders(IContentHelper contentHelper, InvariantHashSet installedMods)
        {
            bool NeedsBasicInfo() => this.IsBasicInfoLoaded;

            // date and weather
            yield return(new ConditionTypeValueProvider(ConditionType.Day, () => SDate.Now().Day.ToString(CultureInfo.InvariantCulture), NeedsBasicInfo, allowedValues: Enumerable.Range(0, 29).Select(p => p.ToString()))); // day 0 = new-game intro

            yield return(new ConditionTypeValueProvider(ConditionType.DayEvent, () => this.GetDayEvent(contentHelper), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.DayOfWeek, () => SDate.Now().DayOfWeek.ToString(), NeedsBasicInfo, allowedValues: Enum.GetNames(typeof(DayOfWeek))));

            yield return(new ConditionTypeValueProvider(ConditionType.DaysPlayed, () => Game1.stats.DaysPlayed.ToString(CultureInfo.InvariantCulture), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.Season, () => SDate.Now().Season, NeedsBasicInfo, allowedValues: new[] { "Spring", "Summer", "Fall", "Winter" }));

            yield return(new ConditionTypeValueProvider(ConditionType.Year, () => SDate.Now().Year.ToString(CultureInfo.InvariantCulture), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.Weather, this.GetCurrentWeather, NeedsBasicInfo, allowedValues: Enum.GetNames(typeof(Weather))));

            // player
            yield return(new ConditionTypeValueProvider(ConditionType.HasFlag, this.GetFlags, NeedsBasicInfo));

            yield return(new HasProfessionValueProvider(NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.HasReadLetter, this.GetReadLetters, NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.HasSeenEvent, this.GetEventsSeen, NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.HasDialogueAnswer, this.GetDialogueAnswers, NeedsBasicInfo));

            yield return(new HasWalletItemValueProvider(NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.IsMainPlayer, () => Context.IsMainPlayer.ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.IsOutdoors, () => Game1.currentLocation?.IsOutdoors.ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.LocationName, () => Game1.currentLocation?.Name, NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.PlayerGender, () => (Game1.player.IsMale ? Gender.Male : Gender.Female).ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.PlayerName, () => Game1.player.Name, NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.PreferredPet, () => (Game1.player.catPerson ? PetType.Cat : PetType.Dog).ToString(), NeedsBasicInfo));

            yield return(new SkillLevelValueProvider(NeedsBasicInfo));

            // relationships
            yield return(new VillagerHeartsValueProvider());

            yield return(new VillagerRelationshipValueProvider());

            yield return(new ConditionTypeValueProvider(ConditionType.Spouse, () => Game1.player?.spouse, NeedsBasicInfo));

            // world
            yield return(new ConditionTypeValueProvider(ConditionType.FarmCave, () => this.GetEnum(Game1.player.caveChoice.Value, FarmCaveType.None).ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.FarmhouseUpgrade, () => Game1.player.HouseUpgradeLevel.ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.FarmName, () => Game1.player.farmName.Value, NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.FarmType, () => this.GetEnum(Game1.whichFarm, FarmType.Custom).ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.IsCommunityCenterComplete, () => this.GetIsCommunityCenterComplete().ToString(), NeedsBasicInfo));

            yield return(new ConditionTypeValueProvider(ConditionType.IsJojaMartComplete, () => this.GetIsJojaMartComplete().ToString(), NeedsBasicInfo));

            yield return(new HavingChildValueProvider(ConditionType.Pregnant, NeedsBasicInfo));

            yield return(new HavingChildValueProvider(ConditionType.HavingChild, NeedsBasicInfo));

            // string manipulation
            yield return(new RangeValueProvider());

            yield return(new LetterCaseValueProvider(ConditionType.Lowercase));

            yield return(new LetterCaseValueProvider(ConditionType.Uppercase));

            // other
            yield return(new ImmutableValueProvider(ConditionType.HasMod.ToString(), installedMods, canHaveMultipleValues: true));

            yield return(new HasValueValueProvider());

            yield return(new ConditionTypeValueProvider(ConditionType.Language, () => contentHelper.CurrentLocaleConstant.ToString(), allowedValues: Enum.GetNames(typeof(LocalizedContentManager.LanguageCode)).Where(p => p != LocalizedContentManager.LanguageCode.th.ToString())));
        }
コード例 #26
0
        /// <summary>Handle the 'patch summary' command.</summary>
        /// <returns>Returns whether the command was handled.</returns>
        private bool HandleSummary()
        {
            StringBuilder output = new StringBuilder();

            // add condition summary
            output.AppendLine();
            output.AppendLine("=====================");
            output.AppendLine("==  Global tokens  ==");
            output.AppendLine("=====================");
            {
                // get data
                var tokensByProvider =
                    (
                        from token in this.TokenManager.GetTokens(enforceContext: false)
                        let inputArgs = token.GetAllowedInputArguments().ToArray()
                                        let rootValues = !token.RequiresInput ? token.GetValues(null).ToArray() : new string[0]
                                                         let isMultiValue =
                            inputArgs.Length > 1 ||
                            rootValues.Length > 1 ||
                            (inputArgs.Length == 1 && token.GetValues(new LiteralString(inputArgs[0])).Count() > 1)
                            orderby isMultiValue, token.Name // single-value tokens first, then alphabetically
                        select token
                    )
                    .GroupBy(p => (p as ModProvidedToken)?.Mod.Name.Trim())
                    .OrderBy(p => p.Key) // default tokens (key is null), then tokens added by other mods
                    .ToArray();
                int labelWidth = Math.Max(tokensByProvider.Max(group => group.Max(p => p.Name.Length)), "token name".Length);

                // group by provider mod (if any)
                foreach (var tokenGroup in tokensByProvider)
                {
                    // print mod name
                    output.AppendLine($"   {tokenGroup.Key ?? "Content Patcher"}:");
                    output.AppendLine();

                    // print table header
                    output.AppendLine($"      {"token name".PadRight(labelWidth)} | value");
                    output.AppendLine($"      {"".PadRight(labelWidth, '-')} | -----");

                    // print tokens
                    foreach (IToken token in tokenGroup)
                    {
                        output.Append($"      {token.Name.PadRight(labelWidth)} | ");

                        if (!token.IsReady)
                        {
                            output.AppendLine("[ ] n/a");
                        }
                        else if (token.RequiresInput)
                        {
                            InvariantHashSet allowedInputs = token.GetAllowedInputArguments();
                            if (allowedInputs.Any())
                            {
                                bool isFirst = true;
                                foreach (string input in allowedInputs.OrderByIgnoreCase(input => input))
                                {
                                    if (isFirst)
                                    {
                                        output.Append("[X] ");
                                        isFirst = false;
                                    }
                                    else
                                    {
                                        output.Append($"      {"".PadRight(labelWidth, ' ')} |     ");
                                    }
                                    output.AppendLine($":{input}: {string.Join(", ", token.GetValues(new LiteralString(input)))}");
                                }
                            }
                            else
                            {
                                output.AppendLine("[X] (token returns a dynamic value)");
                            }
                        }
                        else
                        {
                            output.AppendLine("[X] " + string.Join(", ", token.GetValues(null).OrderByIgnoreCase(p => p)));
                        }
                    }

                    output.AppendLine();
                }
            }

            // add patch summary
            var patches = this.GetAllPatches()
                          .GroupByIgnoreCase(p => p.ContentPack.Manifest.Name)
                          .OrderByIgnoreCase(p => p.Key);

            output.AppendLine(
                "=====================\n"
                + "== Content patches ==\n"
                + "=====================\n"
                + "The following patches were loaded. For each patch:\n"
                + "  - 'loaded' shows whether the patch is loaded and enabled (see details for the reason if not).\n"
                + "  - 'conditions' shows whether the patch matches with the current conditions (see details for the reason if not). If this is unexpectedly false, check (a) the conditions above and (b) your Where field.\n"
                + "  - 'applied' shows whether the target asset was loaded and patched. If you expected it to be loaded by this point but it's false, double-check (a) that the game has actually loaded the asset yet, and (b) your Targets field is correct.\n"
                + "\n"
                );
            foreach (IGrouping <string, PatchInfo> patchGroup in patches)
            {
                ModTokenContext tokenContext = this.TokenManager.TrackLocalTokens(patchGroup.First().ContentPack.Pack);
                output.AppendLine($"{patchGroup.Key}:");
                output.AppendLine("".PadRight(patchGroup.Key.Length + 1, '-'));

                // print tokens
                {
                    var tokens =
                        (
                            // get non-global tokens
                            from IToken token in tokenContext.GetTokens(enforceContext: false)
                            where token.Scope != null

                            // get input arguments
                            let validInputs = token.IsReady && token.RequiresInput
                                ? token.GetAllowedInputArguments().Select(p => new LiteralString(p)).AsEnumerable <ITokenString>()
                                : new ITokenString[] { null }
                            from ITokenString input in validInputs

                            where !token.RequiresInput || validInputs.Any() // don't show tokens which can't be represented

                            // select display data
                            let result = new
                    {
                        Name = token.RequiresInput ? $"{token.Name}:{input}" : token.Name,
                        Value = token.IsReady ? string.Join(", ", token.GetValues(input)) : "",
                        IsReady = token.IsReady
                    }
                            orderby result.Name
                            select result
                        )
                        .ToArray();
                    if (tokens.Any())
                    {
                        int labelWidth = Math.Max(tokens.Max(p => p.Name.Length), "token name".Length);

                        output.AppendLine();
                        output.AppendLine("   Local tokens:");

                        output.AppendLine($"      {"token name".PadRight(labelWidth)} | value");
                        output.AppendLine($"      {"".PadRight(labelWidth, '-')} | -----");

                        foreach (var token in tokens)
                        {
                            output.AppendLine($"      {token.Name.PadRight(labelWidth)} | [{(token.IsReady ? "X" : " ")}] {token.Value}");
                        }
                    }
                }

                // print patches
                output.AppendLine();
                output.AppendLine("   loaded  | conditions | applied | name + details");
                output.AppendLine("   ------- | ---------- | ------- | --------------");
                foreach (PatchInfo patch in patchGroup.OrderByIgnoreCase(p => p.ShortName))
                {
                    // log checkbox and patch name
                    output.Append($"   [{(patch.IsLoaded ? "X" : " ")}]     | [{(patch.MatchesContext ? "X" : " ")}]        | [{(patch.IsApplied ? "X" : " ")}]     | {patch.ShortName}");

                    // log target value if different from name
                    {
                        // get raw value
                        string rawValue = null;
                        if (!patch.ShortName.Contains($"{patch.RawTargetAsset}"))
                        {
                            rawValue = $"{patch.Type} {patch.RawTargetAsset}";
                        }

                        // get parsed value
                        string parsedValue = null;
                        if (patch.MatchesContext && patch.ParsedTargetAsset != null && patch.ParsedTargetAsset.HasAnyTokens)
                        {
                            parsedValue = patch.ParsedTargetAsset.Value;
                        }

                        // format
                        if (rawValue != null || parsedValue != null)
                        {
                            output.Append(" (");
                            if (rawValue != null)
                            {
                                output.Append(rawValue);
                                if (parsedValue != null)
                                {
                                    output.Append(" ");
                                }
                            }
                            if (parsedValue != null)
                            {
                                output.Append($"=> {parsedValue}");
                            }
                            output.Append(")");
                        }
                    }

                    // log reason not applied
                    string errorReason = this.GetReasonNotLoaded(patch);
                    if (errorReason != null)
                    {
                        output.Append($"  // {errorReason}");
                    }

                    // log common issues
                    if (errorReason == null && patch.IsLoaded && !patch.IsApplied && patch.ParsedTargetAsset.IsMeaningful())
                    {
                        string assetName = patch.ParsedTargetAsset.Value;

                        List <string> issues = new List <string>();
                        if (this.AssetNameWithContentPattern.IsMatch(assetName))
                        {
                            issues.Add("shouldn't include 'Content/' prefix");
                        }
                        if (this.AssetNameWithExtensionPattern.IsMatch(assetName))
                        {
                            var match = this.AssetNameWithExtensionPattern.Match(assetName);
                            issues.Add($"shouldn't include '{match.Captures[0]}' extension");
                        }
                        if (this.AssetNameWithLocalePattern.IsMatch(assetName))
                        {
                            issues.Add("shouldn't include language code (use conditions instead)");
                        }

                        if (issues.Any())
                        {
                            output.Append($" // hint: asset name may be incorrect ({string.Join("; ", issues)}).");
                        }
                    }

                    // end line
                    output.AppendLine();
                }
                output.AppendLine(); // blank line between groups
            }

            this.Monitor.Log(output.ToString(), LogLevel.Debug);
            return(true);
        }
コード例 #27
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;
                }
            }
        }
コード例 #28
0
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="name">The token name.</param>
 /// <param name="values">Get the current token values.</param>
 /// <param name="scope">The mod namespace in which the token is accessible, or <c>null</c> for any namespace.</param>
 /// <param name="allowedValues">The allowed values (or <c>null</c> if any value is allowed).</param>
 /// <param name="canHaveMultipleValues">Whether the root may contain multiple values (or <c>null</c> to set it based on the given values).</param>
 public ImmutableToken(string name, InvariantHashSet values, string scope = null, InvariantHashSet allowedValues = null, bool?canHaveMultipleValues = null)
     : base(new ImmutableValueProvider(name, values, allowedValues, canHaveMultipleValues), scope)
 {
 }
コード例 #29
0
 /// <summary>Get the allowed values for a token name (or <c>null</c> if any value is allowed).</summary>
 /// <param name="input">The input argument, if applicable.</param>
 /// <exception cref="InvalidOperationException">The input argument doesn't match this token, or does not respect <see cref="IValueProvider.AllowsInput"/> or <see cref="IValueProvider.RequiresInput"/>.</exception>
 public override InvariantHashSet GetAllowedValues(string input)
 {
     return(input != null
         ? InvariantHashSet.Boolean()
         : null);
 }
コード例 #30
0
        /// <summary>Get the subkey/value pairs used in the given name and values.</summary>
        /// <param name="name">The token name to validate.</param>
        /// <param name="values">The values to validate.</param>
        /// <returns>Returns the subkey/value pairs found. If the <paramref name="name"/> includes a subkey, the <paramref name="values"/> are treated as values of that subkey. Otherwise if <see cref="CanHaveSubkeys"/> is true, then each value is treated as <c>subkey:value</c> (if they contain a colon) or <c>value</c> (with a null subkey).</returns>
        protected IEnumerable <KeyValuePair <TokenName, string> > GetSubkeyValuePairsFor(TokenName name, InvariantHashSet values)
        {
            // no subkeys in values
            if (!this.CanHaveSubkeys || name.HasSubkey())
            {
                foreach (string value in values)
                {
                    yield return(new KeyValuePair <TokenName, string>(name, value));
                }
            }

            // possible subkeys in values
            else
            {
                foreach (string value in values)
                {
                    string[] parts = value.Split(new[] { ':' }, 2);
                    if (parts.Length < 2)
                    {
                        yield return(new KeyValuePair <TokenName, string>(name, parts[0]));
                    }
                    else
                    {
                        yield return(new KeyValuePair <TokenName, string>(new TokenName(name.Key, parts[0]), parts[1]));
                    }
                }
            }
        }