Esempio n. 1
0
        /// <summary>Validate that the provided values are valid for the input argument (regardless of whether they match).</summary>
        /// <param name="input">The input argument, if applicable.</param>
        /// <param name="values">The values to validate.</param>
        /// <param name="error">The validation error, if any.</param>
        /// <returns>Returns whether validation succeeded.</returns>
        public bool TryValidateValues(ITokenString input, InvariantHashSet values, out string error)
        {
            if (!this.TryValidateInput(input, out error))
            {
                return(false);
            }

            // default validation
            {
                InvariantHashSet validValues = this.GetAllowedValues(input);
                if (validValues?.Any() == true)
                {
                    string[] invalidValues = values
                                             .Where(p => !validValues.Contains(p))
                                             .Distinct()
                                             .ToArray();
                    if (invalidValues.Any())
                    {
                        error = $"invalid values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}";
                        return(false);
                    }
                }
            }

            // custom validation
            foreach (string value in values)
            {
                if (!this.TryValidate(input, value, out error))
                {
                    return(false);
                }
            }

            // no issues found
            error = null;
            return(true);
        }
Esempio n. 2
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();
                }
            }

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

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

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

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

                output.AppendLine();
            }


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                        // end line
                        output.AppendLine();
                    }

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

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

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

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

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

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

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

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

            this.Monitor.Log(output.ToString(), LogLevel.Debug);
            return(true);
        }