/// <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)); }); } }
/// <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); }
/// <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); }
/// <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.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 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.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)); }); } }