/// <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(); } } // add patch summary var patches = this.GetAllPatches() .Where(p => !forModIds.Any() || forModIds.Contains(p.ContentPack.Manifest.UniqueID)) .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" + (forModIds.Any() ? $"\n(Filtered to content pack ID{(forModIds.Count > 1 ? "s" : "")}: {string.Join(", ", forModIds.OrderByIgnoreCase(p => p))}.)\n" : "") + "\n" ); foreach (IGrouping <string, PatchInfo> 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 ); var 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)) { 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)})."); } } // log update rate issues if (patch.Patch != null) { foreach (var pair in this.TokenManager.TokensWithSpecialUpdateRates) { UpdateRate rate = pair.Item1; string label = pair.Item2; var 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 IToken[] tokens = ( 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 ) .ToArray(); int labelWidth = Math.Max(tokens.Max(p => p.Name.Length), "token name".Length); // print table header output.AppendLine($" {"token name".PadRight(labelWidth)} | value"); output.AppendLine($" {"".PadRight(labelWidth, '-')} | -----"); // print tokens foreach (IToken token in tokens) { output.Append($" {token.Name.PadRight(labelWidth)} | "); if (!token.IsReady) { output.AppendLine("[ ] n/a"); } else if (token.RequiresInput) { bool isFirst = true; foreach (string input in token.GetAllowedInputArguments().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] " + 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 && token.Name != ConditionType.HasFile.ToString() // 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 // 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.Type} {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()); return(true); }
private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks) { // load content packs ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}")); foreach (RawContentPack current in contentPacks) { this.Monitor.VerboseLog($"Loading content pack '{current.Manifest.Name}'..."); try { ContentConfig content = current.Content; // load tokens ModTokenContext tokenContext = this.TokenManager.TrackLocalTokens(current.ManagedPack.Pack); { // load config.json InvariantDictionary <ConfigField> config = configFileHandler.Read(current.ManagedPack, content.ConfigSchema); configFileHandler.Save(current.ManagedPack, config, this.Helper); if (config.Any()) { this.Monitor.VerboseLog($" found config.json with {config.Count} fields..."); } // load config tokens foreach (KeyValuePair <string, ConfigField> pair in config) { ConfigField field = pair.Value; tokenContext.Add(new ImmutableToken(pair.Key, field.Value, allowedValues: field.AllowValues, canHaveMultipleValues: field.AllowMultiple)); } // load dynamic tokens foreach (DynamicTokenConfig entry in content.DynamicTokens ?? new DynamicTokenConfig[0]) { void LogSkip(string reason) => this.Monitor.Log($"Ignored {current.Manifest.Name} > dynamic token '{entry.Name}': {reason}", LogLevel.Warn); // validate token key if (!TokenName.TryParse(entry.Name, out TokenName name)) { LogSkip("the name could not be parsed as a token key."); continue; } if (name.HasSubkey()) { LogSkip("the token name cannot contain a subkey (:)."); continue; } if (name.TryGetConditionType(out ConditionType conflictingType)) { LogSkip($"conflicts with global token '{conflictingType}'."); continue; } if (config.ContainsKey(name.Key)) { LogSkip($"conflicts with player config token '{conflictingType}'."); continue; } // parse values InvariantHashSet values = entry.Value != null?this.ParseCommaDelimitedField(entry.Value) : new InvariantHashSet(); // parse conditions ConditionDictionary conditions; { if (!this.TryParseConditions(entry.When, tokenContext, current.Migrator, out conditions, out string error)) { this.Monitor.Log($"Ignored {current.Manifest.Name} > '{entry.Name}' token: its {nameof(DynamicTokenConfig.When)} field is invalid: {error}.", LogLevel.Warn); continue; } } // add token tokenContext.Add(new DynamicTokenValue(name, values, conditions)); } } // load patches content.Changes = this.SplitPatches(content.Changes).ToArray(); this.NamePatches(current.ManagedPack, content.Changes); foreach (PatchConfig patch in content.Changes) { this.Monitor.VerboseLog($" loading {patch.LogName}..."); this.LoadPatch(current.ManagedPack, patch, tokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn)); } } catch (Exception ex) { this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); continue; } } }
private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks) { // load content packs ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}", LogLevel.Warn)); foreach (RawContentPack current in contentPacks) { this.Monitor.VerboseLog($"Loading content pack '{current.Manifest.Name}'..."); try { ContentConfig content = current.Content; // load tokens ModTokenContext modContext = this.TokenManager.TrackLocalTokens(current.ManagedPack.Pack); { // load config.json InvariantDictionary <ConfigField> config = configFileHandler.Read(current.ManagedPack, content.ConfigSchema, current.Content.Format); configFileHandler.Save(current.ManagedPack, config, this.Helper); if (config.Any()) { this.Monitor.VerboseLog($" found config.json with {config.Count} fields..."); } // load config tokens foreach (KeyValuePair <string, ConfigField> pair in config) { ConfigField field = pair.Value; modContext.Add(new ImmutableToken(pair.Key, field.Value, scope: current.Manifest.UniqueID, allowedValues: field.AllowValues, canHaveMultipleValues: field.AllowMultiple)); } // load dynamic tokens foreach (DynamicTokenConfig entry in content.DynamicTokens ?? new DynamicTokenConfig[0]) { void LogSkip(string reason) => this.Monitor.Log($"Ignored {current.Manifest.Name} > dynamic token '{entry.Name}': {reason}", LogLevel.Warn); // validate token key if (string.IsNullOrWhiteSpace(entry.Name)) { LogSkip("the token name can't be empty."); continue; } if (entry.Name.Contains(InternalConstants.InputArgSeparator)) { LogSkip($"the token name can't have an input argument ({InternalConstants.InputArgSeparator} character)."); continue; } if (Enum.TryParse <ConditionType>(entry.Name, true, out _)) { LogSkip("the token name is already used by a global token."); continue; } if (config.ContainsKey(entry.Name)) { LogSkip("the token name is already used by a config token."); continue; } // parse values ITokenString values; if (!string.IsNullOrWhiteSpace(entry.Value)) { if (!this.TryParseStringTokens(entry.Value, modContext, current.Migrator, out string valueError, out values)) { LogSkip($"the token value is invalid: {valueError}"); continue; } } else { values = new LiteralString(""); } // parse conditions IList <Condition> conditions; { if (!this.TryParseConditions(entry.When, modContext, current.Migrator, out conditions, out string conditionError)) { this.Monitor.Log($"Ignored {current.Manifest.Name} > '{entry.Name}' token: its {nameof(DynamicTokenConfig.When)} field is invalid: {conditionError}.", LogLevel.Warn); continue; } } // add token modContext.Add(new DynamicTokenValue(entry.Name, values, conditions)); } } // load patches IContext patchTokenContext = new SinglePatchContext(current.Manifest.UniqueID, parentContext: modContext); // make patch tokens available to patches content.Changes = this.SplitPatches(content.Changes).ToArray(); this.NamePatches(current.ManagedPack, content.Changes); foreach (PatchConfig patch in content.Changes) { this.Monitor.VerboseLog($" loading {patch.LogName}..."); this.LoadPatch(current.ManagedPack, patch, patchTokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn)); } } catch (Exception ex) { this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); continue; } } }
/// <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 IToken[] tokens = ( from token in this.TokenManager.GetTokens(enforceContext: false) let subkeys = token.GetSubkeys().ToArray() let rootValues = !token.RequiresSubkeys ? token.GetValues(token.Name).ToArray() : new string[0] let multiValue = subkeys.Length > 1 || rootValues.Length > 1 || (subkeys.Length == 1 && token.GetValues(subkeys[0]).Count() > 1) orderby multiValue, token.Name.Key // single-value tokens first, then alphabetically select token ) .ToArray(); int labelWidth = tokens.Max(p => p.Name.Key.Length); // print table header output.AppendLine($" {"token name".PadRight(labelWidth)} | value"); output.AppendLine($" {"".PadRight(labelWidth, '-')} | -----"); // print tokens foreach (IToken token in tokens) { output.Append($" {token.Name.Key.PadRight(labelWidth)} | "); if (!token.IsReady) { output.AppendLine("[ ] n/a"); } else if (token.RequiresSubkeys) { bool isFirst = true; foreach (TokenName name in token.GetSubkeys().OrderByIgnoreCase(key => key.Subkey)) { if (isFirst) { output.Append("[X] "); isFirst = false; } else { output.Append($" {"".PadRight(labelWidth, ' ')} | "); } output.AppendLine($":{name.Subkey}: {string.Join(", ", token.GetValues(name))}"); } } else { output.AppendLine("[X] " + string.Join(", ", token.GetValues(token.Name).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 { IToken[] localTokens = tokenContext .GetTokens(localOnly: true, enforceContext: false) .Where(p => p.Name.Key != ConditionType.HasFile.ToString()) // no value to display .ToArray(); if (localTokens.Any()) { output.AppendLine(); output.AppendLine(" Local tokens:"); foreach (IToken token in localTokens.OrderBy(p => p.Name)) { if (token.RequiresSubkeys) { foreach (TokenName name in token.GetSubkeys().OrderBy(p => p)) { output.AppendLine($" {name}: {string.Join(", ", token.GetValues(name))}"); } } else { output.AppendLine($" {token.Name}: {string.Join(", ", token.GetValues(token.Name))}"); } } } } // 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 raw target (if not in name) if (!patch.ShortName.Contains($"{patch.Type} {patch.RawTargetAsset}")) { output.Append($" | {patch.Type} {patch.RawTargetAsset}"); } // log parsed target if tokenised if (patch.MatchesContext && patch.ParsedTargetAsset != null && patch.ParsedTargetAsset.Tokens.Any()) { output.Append($" | => {patch.ParsedTargetAsset.Value}"); } // log reason not applied string errorReason = this.GetReasonNotLoaded(patch, tokenContext); if (errorReason != null) { output.Append($" // {errorReason}"); } // log common issues if (errorReason == null && patch.IsLoaded && !patch.IsApplied && patch.ParsedTargetAsset?.Value != null) { 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()); 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("====================="); { IToken[] tokens = this.TokenManager.GetTokens(enforceContext: false).OrderBy(this.GetDisplayOrder).ToArray(); IToken[] tokensOutOfContext = tokens.Where(p => !p.IsValidInContext).ToArray(); foreach (IToken token in tokens.Except(tokensOutOfContext)) { if (token.RequiresSubkeys) { foreach (TokenName name in token.GetSubkeys().OrderBy(p => p)) { output.AppendLine($" {name}: {string.Join(", ", token.GetValues(name))}"); } } else { output.AppendLine($" {token.Name}: {string.Join(", ", token.GetValues(token.Name))}"); } } if (tokensOutOfContext.Any()) { output.AppendLine(); output.AppendLine($" Tokens not valid in this context: {string.Join(", ", tokensOutOfContext.Select(p => p.Name.ToString()))}."); } } output.AppendLine(); // add patch summary var patches = this.GetAllPatches() .GroupBy(p => p.ContentPack.Manifest.Name) .OrderBy(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("========================"); // print tokens { IToken[] localTokens = tokenContext.GetTokens(localOnly: true, enforceContext: false).ToArray(); if (localTokens.Any()) { output.AppendLine(); output.AppendLine(" Local tokens:"); foreach (IToken token in localTokens.OrderBy(p => p.Name)) { if (token.RequiresSubkeys) { foreach (TokenName name in token.GetSubkeys().OrderBy(p => p)) { output.AppendLine($" {name}: {string.Join(", ", token.GetValues(name))}"); } } else { output.AppendLine($" {token.Name}: {string.Join(", ", token.GetValues(token.Name))}"); } } } } // print patches output.AppendLine(); output.AppendLine(" loaded | conditions | applied | name + details"); output.AppendLine(" ------- | ---------- | ------- | --------------"); foreach (PatchInfo patch in patchGroup.OrderBy(p => p.ShortName)) { // log checkbox and patch name output.Append($" [{(patch.IsLoaded ? "X" : " ")}] | [{(patch.MatchesContext ? "X" : " ")}] | [{(patch.IsApplied ? "X" : " ")}] | {patch.ShortName}"); // log raw target (if not in name) if (!patch.ShortName.Contains($"{patch.Type} {patch.RawAssetName}")) { output.Append($" | {patch.Type} {patch.RawAssetName}"); } // log parsed target if tokenised if (patch.MatchesContext && patch.ParsedAssetName != null && patch.ParsedAssetName.Tokens.Any()) { output.Append($" | => {patch.ParsedAssetName.Value}"); } // log reason not applied string errorReason = this.GetReasonNotLoaded(patch, tokenContext); if (errorReason != null) { output.Append($" // {errorReason}"); } // log common issues if (errorReason == null && patch.IsLoaded && !patch.IsApplied && patch.ParsedAssetName?.Value != null) { string assetName = patch.ParsedAssetName.Value; List <string> issues = new List <string>(); if (this.AssetNameWithContentPattern.IsMatch(assetName)) { issues.Add("shouldn't include 'Content/' prefix"); } if (this.AssetNameWithXnbExtensionPattern.IsMatch(assetName)) { issues.Add("shouldn't include '.xnb' extension"); } if (this.AssetNameWithLocalePattern.IsMatch(assetName)) { issues.Add("shouldn't include language code (use conditions instead)"); } if (issues.Any()) { output.Append($" | NOTE: asset name may be incorrect ({string.Join("; ", issues)})."); } } // end line output.AppendLine(); } output.AppendLine(); // blank line between groups } this.Monitor.Log(output.ToString()); return(true); }