/// <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 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); }
/// <summary>Validate that the provided input argument is valid.</summary> /// <param name="input">The input argument, if applicable.</param> /// <param name="error">The validation error, if any.</param> /// <returns>Returns whether validation succeeded.</returns> public bool TryValidateInput(ITokenString input, out string error) { // validate input if (input.IsMeaningful()) { // check if input allowed if (!this.AllowsInput) { error = $"invalid input argument ({input}), token {this.Name} doesn't allow input."; return(false); } // check value InvariantHashSet validInputs = this.GetValidInputs(); if (validInputs?.Any() == true) { if (!validInputs.Contains(input.Value)) { error = $"invalid input argument ({(input.Raw != input.Value ? $"{input.Raw} => {input.Value}" : input.Value)}) for {this.Name} token, expected any of {string.Join(", ", validInputs)}"; return(false); } } } // no issues found error = null; return(true); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="name">The value provider name.</param> /// <param name="values">Get the current token values.</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 ImmutableValueProvider(string name, InvariantHashSet values, InvariantHashSet allowedValues = null, bool?canHaveMultipleValues = null) : base(name, mayReturnMultipleValuesForRoot: false) { this.Values = values ?? new InvariantHashSet(); this.AllowedRootValues = allowedValues?.Any() == true ? allowedValues : null; this.MayReturnMultipleValuesForRoot = canHaveMultipleValues ?? (this.Values.Count > 1 || this.AllowedRootValues == null || this.AllowedRootValues.Count > 1); this.IsMutable = false; }
/// <inheritdoc /> public override bool TryMigrate(Condition condition, out string error) { // 1.19 adds boolean query expressions bool isQuery = condition.Name?.EqualsIgnoreCase(nameof(ConditionType.Query)) == true; if (isQuery) { InvariantHashSet values = condition.Values?.SplitValuesUnique(); if (values?.Any() == true && values.All(p => bool.TryParse(p, out bool _))) { error = "using boolean query expressions"; return(false); } } return(base.TryMigrate(condition, out error)); }
/// <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 TryValidateValues(ITokenString input, InvariantHashSet values, out string error) { if (!this.TryValidateInput(input, out error)) { return(false); } // default validation { InvariantHashSet validValues = this.GetAllowedValues(input); if (validValues?.Any() == true) { string[] invalidValues = values .Where(p => !validValues.Contains(p)) .Distinct() .ToArray(); if (invalidValues.Any()) { error = $"invalid values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; return(false); } } } // custom validation foreach (string value in values) { if (!this.TryValidate(input, value, out error)) { return(false); } } // no issues found error = null; return(true); }
/// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="tokenContext">The tokens available for this content pack.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="minumumTokenVersions">The minimum format versions for newer condition types.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> private bool TryParseConditions(InvariantDictionary <string> raw, IContext tokenContext, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, InvariantDictionary <ISemanticVersion> minumumTokenVersions, out ConditionDictionary conditions, out string error) { conditions = new ConditionDictionary(); // no conditions if (raw == null || !raw.Any()) { error = null; return(true); } // parse conditions foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!TokenName.TryParse(pair.Key, out TokenName name)) { error = $"'{pair.Key}' isn't a valid token name"; conditions = null; return(false); } // get token IToken token = tokenContext.GetToken(name, enforceContext: false); if (token == null) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", tokenContext.GetTokens(enforceContext: false).Select(p => p.Name).OrderBy(p => p))}"; conditions = null; return(false); } // validate subkeys if (!token.CanHaveSubkeys) { if (name.HasSubkey()) { error = $"{name.Key} conditions don't allow subkeys (:)"; conditions = null; return(false); } } else if (token.RequiresSubkeys) { if (!name.HasSubkey()) { error = $"{name.Key} conditions must specify a token subkey (see readme for usage)"; conditions = null; return(false); } } // check compatibility if (minumumTokenVersions.TryGetValue(name.Key, out ISemanticVersion minVersion) && minVersion.IsNewerThan(formatVersion)) { error = $"{name} isn't available with format version {formatVersion} (change the {nameof(ContentConfig.Format)} field to {latestFormatVersion} to use newer features)"; conditions = null; return(false); } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{name} can't be empty"; conditions = null; return(false); } // restrict to allowed values InvariantHashSet rawValidValues = token.GetAllowedValues(name); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {name} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // perform custom validation if (!token.TryCustomValidation(values, out string customError)) { error = $"invalid {name} values: {customError}"; conditions = null; return(false); } // create condition conditions[name] = new Condition(name, values); } // return parsed conditions error = null; return(true); }
/// <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 != null) { 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); }
/// <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 != null) { 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); }
/// <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 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); } // 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); }
/********* ** 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.InputArgSeparator)) { logWarning(rawKey, $"the name '{rawKey}' can't have an input argument ({InternalConstants.InputArgSeparator} 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); } return(schema); }
/// <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); }
/// <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); }
/********* ** 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); }