/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromFile">The normalized asset key from which to load entries (if applicable), including tokens.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="normalizeAssetName">Normalize an asset name.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="patchLoader">Handles loading and unloading patches for content packs.</param> public IncludePatch(LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromFile, UpdateRate updateRate, RawContentPack contentPack, IPatch parentPatch, Func <string, string> normalizeAssetName, IMonitor monitor, PatchLoader patchLoader) : base( path: path, type: PatchType.Include, assetName: assetName, conditions: conditions, fromAsset: fromFile, updateRate: updateRate, parentPatch: parentPatch, contentPack: contentPack.ManagedPack, normalizeAssetName: normalizeAssetName ) { this.RawContentPack = contentPack; this.Monitor = monitor; this.PatchLoader = patchLoader; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="indexPath">The path of indexes from the root <c>content.json</c> to this patch; see <see cref="IPatch.IndexPath"/>.</param> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromFile">The normalized asset key from which to load entries (if applicable), including tokens.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="parseAssetName">Parse an asset name.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="patchLoader">Handles loading and unloading patches for content packs.</param> public IncludePatch(int[] indexPath, LogPathBuilder path, IManagedTokenString?assetName, IEnumerable <Condition> conditions, IManagedTokenString fromFile, UpdateRate updateRate, RawContentPack contentPack, IPatch?parentPatch, Func <string, IAssetName> parseAssetName, IMonitor monitor, PatchLoader patchLoader) : base( indexPath: indexPath, path: path, type: PatchType.Include, assetName: assetName, conditions: conditions, fromAsset: fromFile, updateRate: updateRate, parentPatch: parentPatch, contentPack: contentPack.ContentPack, parseAssetName: parseAssetName ) { this.RawContentPack = contentPack; this.Monitor = monitor; this.PatchLoader = patchLoader; }
/********* ** Protected methods *********/ /// <summary>Construct an instance.</summary> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="type">The patch type.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="normalizeAssetName">Normalize an asset name.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent <see cref="PatchType.Include"/> patch for which this patch was loaded, if any.</param> /// <param name="fromAsset">The normalized asset key from which to load the local asset (if applicable), including tokens.</param> protected Patch(LogPathBuilder path, PatchType type, IManagedTokenString assetName, IEnumerable <Condition> conditions, UpdateRate updateRate, ManagedContentPack contentPack, IPatch parentPatch, Func <string, string> normalizeAssetName, IManagedTokenString fromAsset = null) { this.Path = path; this.Type = type; this.ManagedRawTargetAsset = assetName; this.Conditions = conditions.ToArray(); this.UpdateRate = updateRate; this.NormalizeAssetNameImpl = normalizeAssetName; this.PrivateContext = new LocalContext(scope: contentPack.Manifest.UniqueID); this.ManagedRawFromAsset = fromAsset; this.ContentPack = contentPack; this.ParentPatch = parentPatch; this.Contextuals .Add(this.Conditions) .Add(assetName) .Add(fromAsset); this.ManuallyUpdatedTokens.Add(assetName); this.ManuallyUpdatedTokens.Add(fromAsset); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">The asset key to load from the content pack instead.</param> /// <param name="fromArea">The sprite area from which to read an image.</param> /// <param name="toArea">The sprite area to overwrite.</param> /// <param name="patchMode">Indicates how the image should be patched.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="normalizeAssetName">Normalize an asset name.</param> public EditImagePatch(LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromAsset, TokenRectangle fromArea, TokenRectangle toArea, PatchMode patchMode, UpdateRate updateRate, ManagedContentPack contentPack, IPatch parentPatch, IMonitor monitor, Func <string, string> normalizeAssetName) : base( path: path, type: PatchType.EditImage, assetName: assetName, conditions: conditions, normalizeAssetName: normalizeAssetName, fromAsset: fromAsset, updateRate: updateRate, contentPack: contentPack, parentPatch: parentPatch ) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.Monitor = monitor; this.Contextuals .Add(fromArea) .Add(toArea); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="indexPath">The path of indexes from the root <c>content.json</c> to this patch; see <see cref="IPatch.IndexPath"/>.</param> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">The asset key to load from the content pack instead.</param> /// <param name="fromArea">The sprite area from which to read an image.</param> /// <param name="toArea">The sprite area to overwrite.</param> /// <param name="patchMode">Indicates how the image should be patched.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="parseAssetName">Parse an asset name.</param> public EditImagePatch(int[] indexPath, LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromAsset, TokenRectangle?fromArea, TokenRectangle?toArea, PatchImageMode patchMode, UpdateRate updateRate, IContentPack contentPack, IPatch?parentPatch, IMonitor monitor, Func <string, IAssetName> parseAssetName) : base( indexPath: indexPath, path: path, type: PatchType.EditImage, assetName: assetName, conditions: conditions, parseAssetName: parseAssetName, fromAsset: fromAsset, updateRate: updateRate, contentPack: contentPack, parentPatch: parentPatch ) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.Monitor = monitor; this.Contextuals .Add(fromArea) .Add(toArea); }
/// <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>Construct an instance.</summary> /// <param name="lexTokens">The lexical tokens parsed from the raw string.</param> /// <param name="context">The available token context.</param> /// <param name="path">The path to the value from the root content file.</param> public TokenString(ILexToken[] lexTokens, IContext context, LogPathBuilder path) { // get lexical tokens this.Parts = ( from token in (lexTokens ?? new ILexToken[0]) let input = token is LexTokenToken lexToken && lexToken.HasInputArgs() ? new TokenString(lexToken.InputArgs.Parts, context, path.With(lexToken.Name)) : null select new TokenStringPart(token, input) ) .ToArray(); // set raw value this.Raw = string.Join("", this.Parts.Select(p => p.LexToken)).Trim(); if (string.IsNullOrWhiteSpace(this.Raw)) { this.Value = this.Raw; return; } // extract tokens bool isMutable = false; bool hasTokens = false; foreach (LexTokenToken lexToken in this.GetTokenPlaceholders(this.LexTokens, recursive: true)) { hasTokens = true; IToken token = context.GetToken(lexToken.Name, enforceContext: false); if (token != null) { this.TokensUsed.Add(token.Name); isMutable = isMutable || token.IsMutable; } else { string requiredModId = lexToken.GetProviderModId(); if (!string.IsNullOrWhiteSpace(requiredModId) && !context.IsModInstalled(requiredModId)) { this.State.AddUnavailableModTokens(requiredModId); } else { this.State.AddInvalidTokens(lexToken.Name); } isMutable = true; // can't optimize away the token value if it's invalid } } // set metadata this.IsMutable = isMutable; this.HasAnyTokens = hasTokens; this.IsSingleTokenOnly = TokenString.GetIsSingleTokenOnly(this.Parts); this.Path = path.ToString(); // set initial context if (this.State.IsReady) { this.UpdateContext(context, forceUpdate: true); } }
/// <summary>Construct an instance.</summary> /// <param name="inputArgs">The raw token input arguments.</param> /// <param name="context">The available token context.</param> /// <param name="path">The path to the value from the root content file.</param> public TokenString(LexTokenInput inputArgs, IContext context, LogPathBuilder path) : this(lexTokens : inputArgs?.Parts, context : context, path : path) { }
/********* ** Public methods *********/ /// <summary>Construct an instance from raw text. This constructor bypasses migrations, so it should not be used to parse any values from a content pack.</summary> /// <param name="raw">The raw string before token substitution.</param> /// <param name="context">The available token context.</param> /// <param name="path">The path to the value from the root content file.</param> public TokenString(string raw, IContext context, LogPathBuilder path) : this(lexTokens : new Lexer().ParseBits(raw, impliedBraces : false).ToArray(), context : context, path : path) { }