コード例 #1
0
        /// <summary>Get the dynamic and config token names defined by a content pack.</summary>
        /// <param name="content">The content pack to read.</param>
        private ISet <string> GetLocalTokenNames(ContentConfig content)
        {
            InvariantHashSet names = new InvariantHashSet();

            // dynamic tokens
            if (content.DynamicTokens?.Any() == true)
            {
                foreach (string name in content.DynamicTokens.Select(p => p.Name))
                {
                    if (!string.IsNullOrWhiteSpace(name))
                    {
                        names.Add(name);
                    }
                }
            }

            // config schema
            if (content.ConfigSchema != null)
            {
                foreach (string name in content.ConfigSchema.Select(p => p.Key))
                {
                    if (!string.IsNullOrWhiteSpace(name))
                    {
                        names.Add(name);
                    }
                }
            }

            // exclude tokens that conflict with a built-in condition, which will be ignored
            names.RemoveWhere(p => Enum.TryParse(p, ignoreCase: true, out ConditionType _));

            return(names);
        }
コード例 #2
0
        /*********
        ** Private methods
        *********/
        /// <summary>Get the dynamic and config token names defined by a content pack.</summary>
        /// <param name="content">The content pack to read.</param>
        private ISet <string> GetLocalTokenNames(ContentConfig content)
        {
            InvariantHashSet names = new InvariantHashSet();

            // dynamic tokens
            foreach (string name in content.DynamicTokens.Select(p => p.Name))
            {
                if (!string.IsNullOrWhiteSpace(name))
                {
                    names.Add(name);
                }
            }

            // config schema
            foreach (string name in content.ConfigSchema.Select(p => p.Key))
            {
                if (!string.IsNullOrWhiteSpace(name))
                {
                    names.Add(name);
                }
            }

            // exclude tokens that conflict with a built-in condition, which will be ignored
            names.RemoveWhere(p => this.GetEnum <ConditionType>(p) != null);

            return(names);
        }
コード例 #3
0
        /// <summary>Get the text representation of a token's values.</summary>
        /// <param name="context">Provides access to contextual tokens.</param>
        /// <param name="part">The token string part whose value to fetch.</param>
        /// <param name="unavailableTokens">A list of unavailable or unready token names to update if needed.</param>
        /// <param name="errors">The errors which occurred (if any).</param>
        /// <param name="text">The text representation, if available.</param>
        /// <returns>Returns true if the token is ready and <paramref name="text"/> was set, else false.</returns>
        private bool TryGetTokenText(IContext context, TokenStringPart part, InvariantHashSet unavailableTokens, InvariantHashSet errors, out string text)
        {
            switch (part.LexToken)
            {
            case LexTokenToken lexToken:
                {
                    // get token
                    IToken token = context.GetToken(lexToken.Name, enforceContext: true);
                    if (token == null || !token.IsReady)
                    {
                        unavailableTokens.Add(lexToken.Name);
                        text = null;
                        return(false);
                    }

                    // get token input
                    if (part.Input != null)
                    {
                        // update input
                        part.Input.UpdateContext(context);

                        // check for unavailable tokens
                        string[] unavailableInputTokens = part.Input
                                                          .GetTokensUsed()
                                                          .Where(name => context.GetToken(name, enforceContext: true)?.IsReady != true)
                                                          .ToArray();
                        if (unavailableInputTokens.Any())
                        {
                            foreach (string tokenName in unavailableInputTokens)
                            {
                                unavailableTokens.Add(tokenName);
                            }
                            text = null;
                            return(false);
                        }
                    }

                    // validate input
                    if (!token.TryValidateInput(part.InputArgs, out string error))
                    {
                        errors.Add(error);
                        text = null;
                        return(false);
                    }

                    // get text representation
                    string[] values = token.GetValues(part.InputArgs).ToArray();
                    text = string.Join(", ", values);
                    return(true);
                }

            default:
                text = part.LexToken.ToString();
                return(true);
            }
        }
コード例 #4
0
        /// <summary>Edit a matched asset.</summary>
        /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param>
        public void Edit <T>(IAssetData asset)
        {
            IPatch[] patches = this.GetCurrentEditors(asset).ToArray();
            if (!patches.Any())
            {
                throw new InvalidOperationException($"Can't edit asset key '{asset.AssetName}' because no patches currently apply. This should never happen.");
            }

            InvariantHashSet loggedContentPacks = new InvariantHashSet();

            foreach (IPatch patch in patches)
            {
                if (this.Verbose)
                {
                    this.VerboseLog($"Applied patch \"{patch.LogName}\" to {asset.AssetName}.");
                }
                else if (loggedContentPacks.Add(patch.ContentPack.Manifest.Name))
                {
                    this.Monitor.Log($"{patch.ContentPack.Manifest.Name} edited {asset.AssetName}.", LogLevel.Trace);
                }

                try
                {
                    patch.Edit <T>(asset);
                    patch.IsApplied = true;
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"unhandled exception applying patch: {patch.LogName}.\n{ex}", LogLevel.Error);
                    patch.IsApplied = false;
                }
            }
        }
コード例 #5
0
        /// <inheritdoc />
        public override bool UpdateContext(IContext context)
        {
            return(this.IsChanged(() =>
            {
                bool changed = false;

                foreach (var pair in this.Values)
                {
                    PlayerType type = pair.Key;
                    InvariantHashSet values = pair.Value;
                    InvariantHashSet oldValues = new InvariantHashSet(values);

                    values.Clear();

                    if (this.MarkReady(this.IsPlayerLoaded()))
                    {
                        Farmer player = type == PlayerType.HostPlayer
                            ? Game1.MasterPlayer
                            : Game1.player;

                        if (player != null)
                        {
                            foreach (string value in this.FetchValues(player))
                            {
                                values.Add(value);
                            }
                        }
                    }

                    changed |= this.IsChanged(oldValues, values);
                }

                return changed;
            }));
        }
コード例 #6
0
        /// <summary>Add a dynamic token value to the context.</summary>
        /// <param name="tokenValue">The token to add.</param>
        public void Add(DynamicTokenValue tokenValue)
        {
            // validate
            if (this.ParentContext.Contains(tokenValue.Name, enforceContext: false))
            {
                throw new InvalidOperationException($"Can't register a '{tokenValue}' token because there's a global token with that name.");
            }
            if (this.LocalContext.Contains(tokenValue.Name, enforceContext: false))
            {
                throw new InvalidOperationException($"Can't register a '{tokenValue.Name}' dynamic token because there's a config token with that name.");
            }

            // get (or create) token
            if (!this.DynamicContext.Tokens.TryGetValue(tokenValue.Name, out DynamicToken token))
            {
                this.DynamicContext.Save(token = new DynamicToken(tokenValue.Name, this.Scope));
            }

            // add token value
            token.AddTokensUsed(tokenValue.GetTokensUsed());
            token.AddAllowedValues(tokenValue.Value);
            this.DynamicTokenValues.Add(tokenValue);

            // track tokens which should trigger an update to this token
            Queue <string>   tokenQueue = new Queue <string>(tokenValue.GetTokensUsed());
            InvariantHashSet visited    = new InvariantHashSet();

            while (tokenQueue.Any())
            {
                // get token name
                string tokenName = tokenQueue.Dequeue();
                if (!visited.Add(tokenName))
                {
                    continue;
                }

                // if the current token uses other tokens, they may affect the being added too
                IToken curToken = this.GetToken(tokenName, enforceContext: false);
                foreach (string name in curToken.GetTokensUsed())
                {
                    tokenQueue.Enqueue(name);
                }
                if (curToken is DynamicToken curDynamicToken)
                {
                    foreach (string name in curDynamicToken.GetPossibleTokensUsed())
                    {
                        tokenQueue.Enqueue(name);
                    }
                }

                // add dynamic value as a dependency of the current token
                if (!this.TokenDependents.TryGetValue(curToken.Name, out InvariantHashSet used))
                {
                    this.TokenDependents.Add(curToken.Name, used = new InvariantHashSet());
                }
                used.Add(tokenValue.Name);
            }
        }
コード例 #7
0
        /// <summary>Update the current context.</summary>
        /// <param name="globalContext">The global token context.</param>
        /// <param name="globalChangedTokens">The global token values which changed, or <c>null</c> to update all tokens.</param>
        public void UpdateContext(IContext globalContext, InvariantHashSet globalChangedTokens)
        {
            // get affected tokens (or null to update all tokens)
            InvariantHashSet affectedTokens = null;

            if (globalChangedTokens != null)
            {
                affectedTokens = new InvariantHashSet();
                foreach (string globalToken in globalChangedTokens)
                {
                    foreach (string affectedToken in this.GetTokensAffectedBy(globalToken))
                    {
                        affectedTokens.Add(affectedToken);
                    }
                }

                if (!affectedTokens.Any())
                {
                    return;
                }
            }

            // update local standard tokens
            foreach (IToken token in this.LocalContext.Tokens.Values)
            {
                if (token.IsMutable && affectedTokens?.Contains(token.Name) != false)
                {
                    token.UpdateContext(this);
                }
            }

            // reset dynamic tokens
            // note: since token values are affected by the order they're defined, only updating tokens affected by globalChangedTokens is not trivial.
            foreach (DynamicToken token in this.DynamicContext.Tokens.Values)
            {
                token.SetValue(null);
                token.SetReady(false);
            }
            foreach (DynamicTokenValue tokenValue in this.DynamicTokenValues)
            {
                tokenValue.UpdateContext(this);
                if (tokenValue.IsReady && tokenValue.Conditions.All(p => p.IsMatch(this)))
                {
                    DynamicToken token = this.DynamicContext.Tokens[tokenValue.Name];
                    token.SetValue(tokenValue.Value);
                    token.SetReady(true);
                }
            }
        }
コード例 #8
0
ファイル: ModEntry.cs プロジェクト: karlovukelic/StardewMods
        /// <summary>Prepare a local asset file for a patch to use.</summary>
        /// <param name="pack">The content pack being loaded.</param>
        /// <param name="path">The asset path in the content patch.</param>
        /// <param name="config">The config values to apply.</param>
        /// <param name="conditions">The conditions to apply.</param>
        /// <param name="error">The error reason if preparing the asset fails.</param>
        /// <param name="tokenedPath">The parsed value.</param>
        /// <param name="shouldPreload">Whether to preload assets if needed.</param>
        /// <returns>Returns whether the local asset was successfully prepared.</returns>
        private bool TryPrepareLocalAsset(ManagedContentPack pack, string path, InvariantDictionary <ConfigField> config, ConditionDictionary conditions, out string error, out TokenString tokenedPath, bool shouldPreload)
        {
            // normalise raw value
            path = this.NormaliseLocalAssetPath(pack, path);
            if (path == null)
            {
                error       = $"must set the {nameof(PatchConfig.FromFile)} field for this action type.";
                tokenedPath = null;
                return(false);
            }

            // tokenise
            if (!this.TryParseTokenString(path, config, out string tokenError, out TokenStringBuilder builder))
            {
                error       = $"the {nameof(PatchConfig.FromFile)} is invalid: {tokenError}";
                tokenedPath = null;
                return(false);
            }
            tokenedPath = builder.Build();

            // preload & validate possible file paths
            InvariantHashSet missingFiles = new InvariantHashSet();

            foreach (string localKey in this.ConditionFactory.GetPossibleStrings(tokenedPath, conditions))
            {
                if (!pack.FileExists(localKey))
                {
                    missingFiles.Add(localKey);
                }
                else if (shouldPreload)
                {
                    pack.PreloadIfNeeded(localKey);
                }
            }
            if (missingFiles.Any())
            {
                error = tokenedPath.ConditionTokens.Any() || missingFiles.Count > 1
                    ? $"{nameof(PatchConfig.FromFile)} '{path}' matches files which don't exist ({string.Join(", ", missingFiles.OrderBy(p => p))})."
                    : $"{nameof(PatchConfig.FromFile)} '{path}' matches a file which doesn't exist.";
                tokenedPath = null;
                return(false);
            }

            // looks OK
            error = null;
            return(true);
        }
コード例 #9
0
ファイル: TokenManager.cs プロジェクト: tyyyh2/StardewMods
        /// <summary>Update the current context.</summary>
        /// <param name="changedGlobalTokens">The global tokens which changed value.</param>
        public void UpdateContext(out InvariantHashSet changedGlobalTokens)
        {
            // update global tokens
            changedGlobalTokens = new InvariantHashSet();
            foreach (IToken token in this.GlobalContext.Tokens.Values)
            {
                bool changed =
                    (token.IsMutable && token.UpdateContext(this)) || // token changed state/value
                    (this.IsFirstUpdate && token.IsReady);    // tokens implicitly change to ready on their first update, even if they were ready from creation
                if (changed)
                {
                    changedGlobalTokens.Add(token.Name);
                }
            }

            // update mod contexts
            foreach (ModTokenContext localContext in this.LocalTokens.Values)
            {
                localContext.UpdateContext(changedGlobalTokens);
            }

            this.IsFirstUpdate = false;
        }
コード例 #10
0
        /// <summary>Add a dynamic token value to the context.</summary>
        /// <param name="name">The token name.</param>
        /// <param name="rawValue">The token value to set.</param>
        /// <param name="conditions">The conditions that must match to set this value.</param>
        public void AddDynamicToken(string name, IManagedTokenString rawValue, IEnumerable <Condition> conditions)
        {
            // validate
            if (this.ParentContext.Contains(name, enforceContext: false))
            {
                throw new InvalidOperationException($"Can't register a '{name}' token because there's a global token with that name.");
            }
            if (this.LocalContext.Contains(name, enforceContext: false))
            {
                throw new InvalidOperationException($"Can't register a '{name}' dynamic token because there's a config token with that name.");
            }

            // get (or create) token
            if (!this.DynamicTokens.TryGetValue(name, out ManagedManualToken managed))
            {
                managed = new ManagedManualToken(name, this.Scope);
                this.DynamicTokens[name] = managed;
                this.DynamicContext.Save(managed.Token);
            }

            // create token value handler
            var tokenValue = new DynamicTokenValue(managed, rawValue, conditions);

            string[] tokensUsed = tokenValue.GetTokensUsed().ToArray();

            // save value info
            managed.ValueProvider.AddTokensUsed(tokensUsed);
            managed.ValueProvider.AddAllowedValues(rawValue);
            this.DynamicTokenValues.Add(tokenValue);

            // track tokens which should trigger an update to this token
            Queue <string>   tokenQueue = new Queue <string>(tokensUsed);
            InvariantHashSet visited    = new InvariantHashSet();

            while (tokenQueue.Any())
            {
                // get token name
                string usedTokenName = tokenQueue.Dequeue();
                if (!visited.Add(usedTokenName))
                {
                    continue;
                }

                // if the used token uses other tokens, they may affect the one being added too
                IToken usedToken = this.GetToken(usedTokenName, enforceContext: false);
                foreach (string nextTokenName in usedToken.GetTokensUsed())
                {
                    tokenQueue.Enqueue(nextTokenName);
                }

                // add new token as a dependent of the used token
                if (!this.TokenDependents.TryGetValue(usedToken.Name, out InvariantHashSet used))
                {
                    this.TokenDependents.Add(usedToken.Name, used = new InvariantHashSet());
                }
                used.Add(name);
            }

            // track new token
            this.HasNewTokens = true;
        }
コード例 #11
0
        /// <summary>Update the current context.</summary>
        /// <param name="contentHelper">The content helper which manages game assets.</param>
        /// <param name="language">The current language.</param>
        /// <param name="date">The current in-game date (if applicable).</param>
        /// <param name="weather">The current in-game weather (if applicable).</param>
        /// <param name="spouse">The current player's internal spouse name (if applicable).</param>
        /// <param name="dayEvent">The day event (e.g. wedding or festival) occurring today (if applicable).</param>
        /// <param name="seenEvents">The event IDs which the player has seen.</param>
        /// <param name="mailFlags">The mail flags set for the player.</param>
        /// <param name="friendships">The current player's friendship details.</param>
        public void UpdateContext(IContentHelper contentHelper, LocalizedContentManager.LanguageCode language, SDate date, Weather?weather, string dayEvent, string spouse, int[] seenEvents, string[] mailFlags, IEnumerable <KeyValuePair <string, Friendship> > friendships)
        {
            this.VerboseLog("Propagating context...");

            // update context
            this.ConditionContext.Set(language: language, date: date, weather: weather, dayEvent: dayEvent, spouse: spouse, seenEvents: seenEvents, mailFlags: mailFlags, friendships: friendships);
            IDictionary <ConditionKey, string> tokenisableConditions = this.ConditionContext.GetSingleValueConditions();

            // update patches
            InvariantHashSet reloadAssetNames = new InvariantHashSet();
            string           prevAssetName    = null;

            foreach (IPatch patch in this.Patches.OrderBy(p => p.AssetName).ThenBy(p => p.LogName))
            {
                // log asset name
                if (this.Verbose && prevAssetName != patch.AssetName)
                {
                    this.VerboseLog($"   {patch.AssetName}:");
                    prevAssetName = patch.AssetName;
                }

                // track old values
                string wasAssetName = patch.AssetName;
                bool   wasApplied   = patch.MatchesContext;

                // update patch
                bool changed     = patch.UpdateContext(this.ConditionContext, tokenisableConditions);
                bool shouldApply = patch.MatchesContext;

                // track patches to reload
                bool reload = (wasApplied && changed) || (!wasApplied && shouldApply);
                if (reload)
                {
                    patch.IsApplied = false;
                    if (wasApplied)
                    {
                        reloadAssetNames.Add(wasAssetName);
                    }
                    if (shouldApply)
                    {
                        reloadAssetNames.Add(patch.AssetName);
                    }
                }

                // log change
                if (this.Verbose)
                {
                    IList <string> changes = new List <string>();
                    if (wasApplied != shouldApply)
                    {
                        changes.Add(shouldApply ? "enabled" : "disabled");
                    }
                    if (wasAssetName != patch.AssetName)
                    {
                        changes.Add($"target: {wasAssetName} => {patch.AssetName}");
                    }
                    string changesStr = string.Join(", ", changes);

                    this.VerboseLog($"      [{(shouldApply ? "X" : " ")}] {patch.LogName}: {(changes.Any() ? changesStr : "OK")}");
                }
            }

            // rebuild asset name lookup
            this.PatchesByCurrentTarget = new InvariantDictionary <HashSet <IPatch> >(
                from patchGroup in this.Patches.GroupBy(p => p.AssetName, StringComparer.InvariantCultureIgnoreCase)
                let key                         = patchGroup.Key
                                      let value = new HashSet <IPatch>(patchGroup)
                                                  select new KeyValuePair <string, HashSet <IPatch> >(key, value)
                );

            // reload assets if needed
            if (reloadAssetNames.Any())
            {
                this.VerboseLog($"   reloading {reloadAssetNames.Count} assets: {string.Join(", ", reloadAssetNames.OrderBy(p => p))}");
                contentHelper.InvalidateCache(asset =>
                {
                    this.VerboseLog($"      [{(reloadAssetNames.Contains(asset.AssetName) ? "X" : " ")}] reload {asset.AssetName}");
                    return(reloadAssetNames.Contains(asset.AssetName));
                });
            }
        }
コード例 #12
0
        /// <summary>Update the current context.</summary>
        /// <param name="contentHelper">The content helper through which to invalidate assets.</param>
        public void UpdateContext(IContentHelper contentHelper)
        {
            this.VerboseLog("Propagating context...");

            // update patches
            InvariantHashSet reloadAssetNames = new InvariantHashSet();
            string           prevAssetName    = null;

            foreach (IPatch patch in this.Patches.OrderByIgnoreCase(p => p.AssetName).ThenByIgnoreCase(p => p.LogName))
            {
                // log asset name
                if (this.Verbose && prevAssetName != patch.AssetName)
                {
                    this.VerboseLog($"   {patch.AssetName}:");
                    prevAssetName = patch.AssetName;
                }

                // track old values
                string wasAssetName = patch.AssetName;
                bool   wasApplied   = patch.MatchesContext;

                // update patch
                IContext tokenContext = this.TokenManager.TrackLocalTokens(patch.ContentPack.Pack);
                bool     changed      = patch.UpdateContext(tokenContext);
                bool     shouldApply  = patch.MatchesContext;

                // track patches to reload
                bool reload = (wasApplied && changed) || (!wasApplied && shouldApply);
                if (reload)
                {
                    patch.IsApplied = false;
                    if (wasApplied)
                    {
                        reloadAssetNames.Add(wasAssetName);
                    }
                    if (shouldApply)
                    {
                        reloadAssetNames.Add(patch.AssetName);
                    }
                }

                // log change
                if (this.Verbose)
                {
                    IList <string> changes = new List <string>();
                    if (wasApplied != shouldApply)
                    {
                        changes.Add(shouldApply ? "enabled" : "disabled");
                    }
                    if (wasAssetName != patch.AssetName)
                    {
                        changes.Add($"target: {wasAssetName} => {patch.AssetName}");
                    }
                    string changesStr = string.Join(", ", changes);

                    this.VerboseLog($"      [{(shouldApply ? "X" : " ")}] {patch.LogName}: {(changes.Any() ? changesStr : "OK")}");
                }

                // warn for invalid load patch
                if (patch is LoadPatch loadPatch && patch.MatchesContext && !patch.ContentPack.FileExists(loadPatch.LocalAsset.Value))
                {
                    this.Monitor.Log($"Patch error: {patch.LogName} has a {nameof(PatchConfig.FromFile)} which matches non-existent file '{loadPatch.LocalAsset.Value}'.", LogLevel.Error);
                }
            }

            // rebuild asset name lookup
            this.PatchesByCurrentTarget = new InvariantDictionary <HashSet <IPatch> >(
                from patchGroup in this.Patches.GroupByIgnoreCase(p => p.AssetName)
                let key                         = patchGroup.Key
                                      let value = new HashSet <IPatch>(patchGroup)
                                                  select new KeyValuePair <string, HashSet <IPatch> >(key, value)
                );

            // reload assets if needed
            if (reloadAssetNames.Any())
            {
                this.VerboseLog($"   reloading {reloadAssetNames.Count} assets: {string.Join(", ", reloadAssetNames.OrderByIgnoreCase(p => p))}");
                contentHelper.InvalidateCache(asset =>
                {
                    this.VerboseLog($"      [{(reloadAssetNames.Contains(asset.AssetName) ? "X" : " ")}] reload {asset.AssetName}");
                    return(reloadAssetNames.Contains(asset.AssetName));
                });
            }
        }
コード例 #13
0
        /// <summary>Prepare a local asset file for a patch to use.</summary>
        /// <param name="pack">The content pack being loaded.</param>
        /// <param name="path">The asset path in the content patch.</param>
        /// <param name="config">The config values to apply.</param>
        /// <param name="conditions">The conditions to apply.</param>
        /// <param name="error">The error reason if preparing the asset fails.</param>
        /// <param name="tokenedPath">The parsed value.</param>
        /// <param name="checkOnly">Prepare the asset info and check if it's valid, but don't actually preload the asset.</param>
        /// <returns>Returns whether the local asset was successfully prepared.</returns>
        private bool TryPrepareLocalAsset(IContentPack pack, string path, InvariantDictionary <ConfigField> config, ConditionDictionary conditions, out string error, out TokenString tokenedPath, bool checkOnly)
        {
            // normalise raw value
            path = this.NormaliseLocalAssetPath(pack, path);
            if (path == null)
            {
                error       = $"must set the {nameof(PatchConfig.FromFile)} field for this action type.";
                tokenedPath = null;
                return(false);
            }

            // tokenise
            if (!this.TryParseTokenString(path, config, out string tokenError, out TokenStringBuilder builder))
            {
                error       = $"the {nameof(PatchConfig.FromFile)} is invalid: {tokenError}";
                tokenedPath = null;
                return(false);
            }
            tokenedPath = builder.Build();

            // validate all possible files exist
            // + preload PNG assets to avoid load-in-draw-loop error
            InvariantHashSet missingFiles = new InvariantHashSet();

            foreach (string localKey in this.ConditionFactory.GetPossibleStrings(tokenedPath, conditions))
            {
                // check-only mode
                if (checkOnly)
                {
                    if (!this.AssetLoader.FileExists(pack, localKey))
                    {
                        missingFiles.Add(localKey);
                    }
                    continue;
                }

                // else preload
                try
                {
                    if (this.AssetLoader.PreloadIfNeeded(pack, localKey))
                    {
                        this.VerboseLog($"      preloaded {localKey}.");
                    }
                }
                catch (FileNotFoundException)
                {
                    missingFiles.Add(localKey);
                }
            }
            if (missingFiles.Any())
            {
                error = tokenedPath.ConditionTokens.Any() || missingFiles.Count > 1
                    ? $"{nameof(PatchConfig.FromFile)} '{path}' matches files which don't exist ({string.Join(", ", missingFiles.OrderBy(p => p))})."
                    : $"{nameof(PatchConfig.FromFile)} '{path}' matches a file which doesn't exist.";
                tokenedPath = null;
                return(false);
            }

            // looks OK
            error = null;
            return(true);
        }