Example #1
0
        /// <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);
        }
Example #2
0
        /// <summary>Handle the 'patch summary' command.</summary>
        /// <returns>Returns whether the command was handled.</returns>
        private bool HandleSummary()
        {
            StringBuilder output = new StringBuilder();

            // add condition summary
            output.AppendLine();
            output.AppendLine("=====================");
            output.AppendLine("==  Global tokens  ==");
            output.AppendLine("=====================");
            {
                // get data
                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);
        }
Example #3
0
        private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks)
        {
            // load content packs
            ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}"));

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

                try
                {
                    ContentConfig content = current.Content;

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

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

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

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

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

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

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

                    // load patches
                    content.Changes = this.SplitPatches(content.Changes).ToArray();
                    this.NamePatches(current.ManagedPack, content.Changes);
                    foreach (PatchConfig patch in content.Changes)
                    {
                        this.Monitor.VerboseLog($"   loading {patch.LogName}...");
                        this.LoadPatch(current.ManagedPack, patch, tokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn));
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error);
                    continue;
                }
            }
        }
Example #4
0
        private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks)
        {
            // load content packs
            ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}", LogLevel.Warn));

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

                try
                {
                    ContentConfig content = current.Content;

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

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

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

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

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

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

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

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

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

                    foreach (PatchConfig patch in content.Changes)
                    {
                        this.Monitor.VerboseLog($"   loading {patch.LogName}...");
                        this.LoadPatch(current.ManagedPack, patch, patchTokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn));
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error);
                    continue;
                }
            }
        }
Example #5
0
        /// <summary>Handle the 'patch summary' command.</summary>
        /// <returns>Returns whether the command was handled.</returns>
        private bool HandleSummary()
        {
            StringBuilder output = new StringBuilder();

            // add condition summary
            output.AppendLine();
            output.AppendLine("=====================");
            output.AppendLine("==  Global tokens  ==");
            output.AppendLine("=====================");
            {
                // get data
                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);
        }