/// <summary>Get whether the token always chooses from a set of known values for the given input. Mutually exclusive with <see cref="HasBoundedRangeValues"/>.</summary> /// <param name="input">The input argument, if applicable.</param> /// <param name="allowedValues">The possible values for the input.</param> public bool HasBoundedValues(string input, out IEnumerable <string> allowedValues) { RecolorTokenArguments inputData = RecolorTokenArguments.Parse(input); string generatedFilePath = GenerateFilePath(inputData); allowedValues = new[] { generatedFilePath }; return(true); }
/// <summary>Generates a file path from token arguments.</summary> private string GenerateFilePath(RecolorTokenArguments inputData) { // "gamecontent" has no file extension, append ".png". string outputPath = inputData.SourcePath.ToLowerInvariant() == "gamecontent" ? $"{inputData.AssetName}.png" : inputData.SourcePath; // Encode configuration to avoid file name collisions. string suffix = $"recolored_{inputData.GetUniqueFileSuffix()}"; return(Utility.AddFileNameSuffix(Path.Combine("generated", outputPath), suffix)); }
/// <summary>Get the current values.</summary> /// <param name="input">The input argument, if applicable.</param> public IEnumerable <string> GetValues(string input) { RecolorTokenArguments inputData = RecolorTokenArguments.Parse(input); if (!(inputData.Equals(previousInputData_))) { mustUpdateContext_ = true; } previousInputData_ = inputData; IContentPack contentPack = Utility.GetContentPackFromModInfo(helper_.ModRegistry.Get(inputData.ContentPackName)); monitor_.Log($"Content pack {contentPack.Manifest.UniqueID} requests recoloring of {inputData.AssetName}."); monitor_.Log($"Recolor with {inputData.MaskPath} and {Utility.ColorToHtml(inputData.BlendColor)}"); // "gamecontent" means loading from game folder. Texture2D source = inputData.SourcePath.ToLowerInvariant() == "gamecontent" ? helper_.Content.Load <Texture2D>(inputData.AssetName, ContentSource.GameContent) : contentPack.LoadAsset <Texture2D>(inputData.SourcePath); Texture2D extracted = inputData.MaskPath.ToLowerInvariant() != "none" ? ExtractSubImage(source, contentPack.LoadAsset <Texture2D>(inputData.MaskPath), inputData.DesaturationMode) : source; Texture2D target = ColorBlend(extracted, inputData.BlendColor); // ATTENTION: In order to load files we just generated we need at least ContentPatcher 1.18.3 . string generatedFilePath = GenerateFilePath(inputData); string generatedFilePathAbsolute = Path.Combine(contentPack.DirectoryPath, generatedFilePath); Directory.CreateDirectory(Path.GetDirectoryName(generatedFilePathAbsolute)); using (FileStream fs = new FileStream(generatedFilePathAbsolute, FileMode.Create)) { target.SaveAsPng(fs, target.Width, target.Height); fs.Close(); } monitor_.Log($"Generated file {generatedFilePathAbsolute}, returning relative path {generatedFilePath}"); helper_.Content.InvalidateCache(inputData.AssetName); yield return(generatedFilePath); }
/// <summary>Get the current values.</summary> /// <param name="input">The input argument, if applicable.</param> public IEnumerable <string> GetValues(string input) { RecolorTokenArguments inputData = RecolorTokenArguments.Parse(input); IContentPack contentPack = Utility.GetContentPackFromModInfo(helper_.ModRegistry.Get(inputData.ContentPackName)); // ATTENTION: In order to load files we just generated we need at least ContentPatcher 1.18.3 . string generatedFilePath = GenerateFilePath(inputData); string generatedFilePathAbsolute = Path.Combine(contentPack.DirectoryPath, generatedFilePath); if (!(inputData.Equals(previousInputData_))) { mustUpdateContext_ = true; previousInputData_ = inputData; monitor_.Log($"Content pack {contentPack.Manifest.UniqueID} requests recoloring of {inputData.AssetName}."); monitor_.Log($"Recolor with {inputData.MaskPath} and {Utility.ColorToHtml(inputData.BlendColor)}, flip mode {inputData.FlipMode}, brightness {inputData.Brightness}"); // Check versions: If version of SDV or content pack changed we have to delete generated images. string contentPackVersion = contentPack.Manifest.Version.ToString(); string generatedDirectoryPathAbsolute = Path.Combine(contentPack.DirectoryPath, "generated"); var versions = contentPack.ReadJsonFile <Dictionary <string, string> >("generated/versions.json") ?? new Dictionary <string, string>(); if (!versions.TryGetValue("StardewValley", out string stardewVersion) || stardewVersion != Game1.version) { versions["StardewValley"] = Game1.version; monitor_.Log($"Version of StardewValley changed from {stardewVersion ?? "(null)"} to {Game1.version}, deleting generated files."); if (Directory.Exists(generatedDirectoryPathAbsolute)) { Directory.Delete(generatedDirectoryPathAbsolute, true); } contentPack.WriteJsonFile("generated/versions.json", versions); } if (!versions.TryGetValue(contentPack.Manifest.UniqueID, out string modVersion) || modVersion != contentPackVersion) { versions[contentPack.Manifest.UniqueID] = contentPackVersion; monitor_.Log($"Version of content pack {contentPack.Manifest.UniqueID} changed from {modVersion ?? "(null)"} to {contentPackVersion}, deleting generated files."); if (Directory.Exists(generatedDirectoryPathAbsolute)) { Directory.Delete(generatedDirectoryPathAbsolute, true); } contentPack.WriteJsonFile("generated/versions.json", versions); } // Skip actions if file was found. if (File.Exists(generatedFilePathAbsolute)) { monitor_.Log($"Found existing file {generatedFilePathAbsolute}, returning relative path {generatedFilePath}"); helper_.Content.InvalidateCache(inputData.AssetName); } else { try { // "gamecontent" means loading from game folder. Don't dispose an asset source! Texture2D source; if (inputData.SourcePath.ToLowerInvariant() == "gamecontent") { // ATTENTION: Game content requires special attention because the loaded assets modify themselves over time: // Game content contains vanilla assets only when game is loaded, otherwise the assets are already patched. // Luma desaturation and recoloring don't change brightness so they can be applied multiple times // without bad effects but changing brightness multiple times changes colors over time. // A way to prevent that is caching them in files once and using the cached versions as a base for modifications. string generatedBasePath = Path.Combine("generated", $"{inputData.AssetName}_gamecontent.png"); string generatedBasePathAbsolute = Path.Combine(contentPack.DirectoryPath, generatedBasePath); Directory.CreateDirectory(Path.GetDirectoryName(generatedBasePathAbsolute)); if (File.Exists(generatedBasePathAbsolute)) { using (FileStream fs = new FileStream(generatedBasePathAbsolute, FileMode.Open)) { source = Texture2D.FromStream(Game1.graphics.GraphicsDevice, fs); } monitor_.Log($"Loading asset {inputData.AssetName} from existing cache file {generatedBasePathAbsolute}"); } else { source = helper_.Content.Load <Texture2D>(inputData.AssetName, ContentSource.GameContent); using (FileStream fs = new FileStream(generatedBasePathAbsolute, FileMode.Create)) { source.SaveAsPng(fs, source.Width, source.Height); fs.Close(); } monitor_.Log($"Saving asset {inputData.AssetName} in cache file {generatedBasePathAbsolute}"); } } else { source = contentPack.LoadAsset <Texture2D>(inputData.SourcePath); } Texture2D mask = inputData.MaskPath.ToLowerInvariant() != "none" ? contentPack.LoadAsset <Texture2D>(inputData.MaskPath) : null; using (Texture2D extracted = ExtractSubImage(source, mask, inputData.DesaturationMode, inputData.Brightness)) using (Texture2D blended = ColorBlend(extracted, inputData.BlendColor)) using (Texture2D flipped = FlipImage(source, blended, inputData.FlipMode)) { Directory.CreateDirectory(Path.GetDirectoryName(generatedFilePathAbsolute)); using (FileStream fs = new FileStream(generatedFilePathAbsolute, FileMode.Create)) { flipped.SaveAsPng(fs, flipped.Width, flipped.Height); fs.Close(); } } monitor_.Log($"Generated file {generatedFilePathAbsolute}, returning relative path {generatedFilePath}"); helper_.Content.InvalidateCache(inputData.AssetName); } catch (ContentLoadException) { // Asset is not available, return its name to prevent game from crashing. monitor_.Log($"Ignoring unavailable asset {inputData.AssetName}. If this was caused by patch reload you can ignore it, the next 10min update cycle should do a proper reload.", LogLevel.Info); generatedFilePath = inputData.AssetName; } } } yield return(generatedFilePath); }