using System; using System.IO; using System.IO.Compression; using System.Collections.Generic; using System.Diagnostics; using System.Text.RegularExpressions; using DesertPaintCodex.Util; using DesertPaintCodex.Services; namespace DesertPaintCodex.Models { public class PlayerProfile { private const string PaintRecipeFile = "dp_recipes.txt"; private const string RibbonRecipeFile = "dp_ribbons.txt"; private readonly string _reactFile; private readonly string _settingsFile; private readonly string _clipFile; private static readonly Regex _recipeHeaderRegex = new(@"^--- Recipe: (?(\w*\s)*\w+)\s*"); private static readonly Regex _recipeIngredientRegex = new(@"(?(\w+\s)?\w+)\s*\|\s*(?\d+)\s*"); private Settings ProfileSettings { get; } = new(); public string Directory { get; } public string Name { get; private set; } public ReactionSet Reactions { get; } = new(); public Dictionary> Clippers { get; } = new(); public string ReagentFile { get; } public Dictionary Recipes { get; } = new(); public Dictionary RibbonRecipes { get; } = new(); public int RecipeCount { get { int count = 0; foreach (PaintRecipe recipe in Recipes.Values) { if (recipe.IsValidForConcentration(PaintRecipe.PaintRecipeMinConcentration)) { ++count; } } return count; } } public int RibbonCount { get { int count = 0; foreach (PaintRecipe recipe in RibbonRecipes.Values) { if (recipe.IsValidForConcentration(PaintRecipe.RibbonRecipeMinConcentration)) { ++count; } } return count; } } public PlayerProfile(string name, string directory) { Name = name; Directory = directory; _reactFile = Path.Combine(directory, "dp_reactions.txt"); ReagentFile = Path.Combine(directory, "ingredients.txt"); _settingsFile = Path.Combine(directory, "settings"); _clipFile = Path.Combine(directory, "clips.txt"); foreach (PaintColor color in PaletteService.Colors) { Recipes.Add(color.Name, new PaintRecipe()); } foreach (PaintColor color in PaletteService.Colors) { RibbonRecipes.Add(color.Name, new PaintRecipe()); } } public bool Initialize() { // Copy template files into new directory. string? templatePath = FileUtils.FindApplicationResourceDirectory("template"); if (templatePath == null) { return false; } // Create new directory. System.IO.Directory.CreateDirectory(Directory); DirectoryInfo di = new(templatePath); FileInfo[] templateFiles = di.GetFiles(); foreach (FileInfo file in templateFiles) { string destFile = Path.Combine(Directory, file.Name); File.Copy(file.FullName, destFile, true); if (!File.Exists(destFile)) return false; } return true; } private static void WriteReaction(TextWriter writer, string reagent1, string reagent2, string r, string g, string b) { writer.Write(reagent1); writer.Write(" "); writer.Write(reagent2); writer.Write(" "); writer.Write(r); writer.Write(" "); writer.Write(g); writer.Write(" "); writer.WriteLine(b); } public static void ConvertFromPP(string ppFile, string dpFile) { using StreamReader reader = new(ppFile); using StreamWriter writer = new(dpFile, false); string? line; while ((line = reader.ReadLine()) != null) { string[] tokens = line.Split('|'); //if ((tokens.Length > 0) && (tokens [0] != "//")) if ((tokens.Length == 5) && (tokens[0].Trim() != "//")) { string reagent1Name = tokens[0].Trim(); string reagent2Name = tokens[1].Trim(); string colorCode = tokens[2].Trim(); string change1 = tokens[3].Trim(); string change2 = tokens[4].Trim(); Reagent reagent1 = ReagentService.GetReagent(reagent1Name); Reagent reagent2 = ReagentService.GetReagent(reagent2Name); if (reagent1 == null || reagent2 == null) continue; bool hasChange1 = int.TryParse(change1, out _); bool hasChange2 = int.TryParse(change2, out _); // Write reaction. switch (colorCode) { case "W": if (hasChange1) { WriteReaction(writer, reagent1Name, reagent2Name, change1, change1, change1); } if (hasChange2) { WriteReaction(writer, reagent2Name, reagent1Name, change2, change2, change2); } break; case "R": if (hasChange1) { WriteReaction(writer, reagent1Name, reagent2Name, change1, "0", "0"); } if (hasChange2) { WriteReaction(writer, reagent2Name, reagent1Name, change2, "0", "0"); } break; case "G": if (hasChange1) { WriteReaction(writer, reagent1Name, reagent2Name, "0", change1, "0"); } if (hasChange2) { WriteReaction(writer, reagent2Name, reagent1Name, "0", change2, "0"); } break; case "B": if (hasChange1) { WriteReaction(writer, reagent1Name, reagent2Name, "0", "0", change1); } if (hasChange2) { WriteReaction(writer, reagent2Name, reagent1Name, "0", "0", change2); } break; } } } } public bool SaveToPP(string ppFile) { Reaction? reaction1, reaction2; using (StreamWriter writer = new(ppFile)) { foreach (string reagentName1 in ReagentService.Names) { // TODO: could be more efficient by only iterating over the names after reagent1 foreach (string reagentName2 in ReagentService.Names) { if (reagentName1.Equals(reagentName2)) continue; Reagent reagent1 = ReagentService.GetReagent(reagentName1); Reagent reagent2 = ReagentService.GetReagent(reagentName2); reaction1 = Reactions.Find(reagent1, reagent2); if (reaction1 is not {Exported: false}) continue; reaction2 = Reactions.Find(reagent2, reagent1); if (reaction2 == null) continue; writer.Write(reagent1.PracticalPaintName + " | " + reagent2.PracticalPaintName + " | "); if ((Math.Abs(reaction1.Red) > Math.Abs(reaction1.Green)) || (Math.Abs(reaction2.Red) > Math.Abs(reaction2.Green))) { writer.WriteLine("R | " + reaction1.Red + " | " + reaction2.Red); } else if ((Math.Abs(reaction1.Green) > Math.Abs(reaction1.Red)) || (Math.Abs(reaction2.Green) > Math.Abs(reaction2.Red))) { writer.WriteLine("G | " + reaction1.Green + " | " + reaction2.Green); } else if ((Math.Abs(reaction1.Blue) > Math.Abs(reaction1.Red)) || (Math.Abs(reaction2.Blue) > Math.Abs(reaction2.Red))) { writer.WriteLine("B | " + reaction1.Blue + " | " + reaction2.Blue); } else { writer.WriteLine("W | " + reaction1.Red + " | " + reaction2.Red); } reaction1.Exported = true; reaction2.Exported = true; } } } // Clear Exported flags. foreach (string reagentName1 in ReagentService.Names) { // TODO: could be more efficient by only iterating over the names after reagent1 foreach (string reagentName2 in ReagentService.Names) { if (reagentName1.Equals(reagentName2)) { continue; } Reagent reagent1 = ReagentService.GetReagent(reagentName1); Reagent reagent2 = ReagentService.GetReagent(reagentName2); reaction1 = Reactions.Find(reagent1, reagent2); if (reaction1 != null) { reaction1.Exported = false; } reaction2 = Reactions.Find(reagent2, reagent1); if (reaction2 != null) { reaction2.Exported = false; } } } return true; } public void ImportFromPP(string reactionsFile) { // Convert old file. ConvertFromPP(reactionsFile, _reactFile); try { // If there is an ingredients file, move it in. string importDir = Path.GetDirectoryName(reactionsFile) ?? ""; File.Copy( Path.Combine(importDir, "ingredients.txt"), Path.Combine(Directory, "ingredients.txt"), true); } catch (Exception) { // If there is no ingredients file, we don't really care. } } public void Import(string file) { if (!File.Exists(file)) { Debug.WriteLine("Import file does not exist: " + file); // TODO: Show message dialog. } ZipFile.ExtractToDirectory(file, Directory, true); } public void Export(string file) { ZipFile.CreateFromDirectory(Directory, file); } public bool Load() { string? line; ProfileSettings.Reset(); ProfileSettings.Load(_settingsFile); Reactions.Clear(); if (File.Exists(ReagentFile)) { ReagentService.LoadProfileReagents(ReagentFile); } else { return false; } ReagentService.InitializeReactions(Reactions); if (!File.Exists(_reactFile)) { return false; } using (StreamReader reader = new(_reactFile)) { while ((line = reader.ReadLine()) != null) { string[] tokens = line.Split(' '); if (tokens.Length == 5) { Reagent reagent1 = ReagentService.GetReagent(tokens[0].Trim()); Reagent reagent2 = ReagentService.GetReagent(tokens[1].Trim()); Reaction reaction = new( int.Parse(tokens[2].Trim()), int.Parse(tokens[3].Trim()), int.Parse(tokens[4].Trim()) ); Reactions.Set(reagent1, reagent2, reaction); } } } if (!File.Exists(_clipFile)) return true; { using StreamReader reader = new(_clipFile); while ((line = reader.ReadLine()) != null) { string[] tokens = line.Split(' '); if (tokens.Length != 3) continue; string reagent1 = tokens[0].Trim(); if (!Clippers.ContainsKey(reagent1)) { Clippers.Add(reagent1, new Dictionary()); } Clippers[reagent1][tokens[1].Trim()] = (ClipType)int.Parse(tokens[2].Trim()); } } return true; } public void Save() { ProfileSettings.Save(_settingsFile); Reaction? reaction; using (StreamWriter writer = new(_reactFile, false)) { foreach (string reagentName1 in ReagentService.Names) { // TODO: could be more efficient by only iterating over the names after reagent1 foreach (string reagentName2 in ReagentService.Names) { if (reagentName1.Equals(reagentName2)) { continue; } Reagent reagent1 = ReagentService.GetReagent(reagentName1); Reagent reagent2 = ReagentService.GetReagent(reagentName2); reaction = Reactions.Find(reagent1, reagent2); if (reaction != null) { writer.WriteLine(reagent1.PracticalPaintName + " " + reagent2.PracticalPaintName + " " + reaction.Red + " " + reaction.Green + " " + reaction.Blue); } } } } using (StreamWriter writer = new(_clipFile, false)) { foreach (var item1 in Clippers) { foreach (var item2 in item1.Value) { if (item2.Value == ClipType.None) continue; writer.WriteLine(item1.Key + " " + item2.Key + " " + (int)item2.Value); } } } } public ClipType PairClipStatus(Reagent reagent1, Reagent reagent2) { if (Clippers.TryGetValue(reagent1.PracticalPaintName, out var item1)) { if (item1.TryGetValue(reagent2.PracticalPaintName, out var clipType)) { return clipType; } } return ClipType.None; } public void SetPairClipStatus(Reagent reagent1, Reagent reagent2, ClipType clip) { if (Clippers.TryGetValue(reagent1.PracticalPaintName, out var item1)) { if (item1.TryGetValue(reagent2.PracticalPaintName, out var clipType)) { if (clipType == clip) return; } } else { item1 = new Dictionary(); Clippers.Add(reagent1.PracticalPaintName, item1); } item1[reagent2.PracticalPaintName] = clip; Save(); } private void LoadRecipes(Dictionary recipeDict, string filename, uint concentration) { foreach (PaintRecipe recipe in recipeDict.Values) { recipe.Clear(); } string recipeFile = Path.Combine(Directory, filename); bool inRecipe = false; PaintRecipe testRecipe = new(); string? currentRecipeColor = null; if (!File.Exists(recipeFile)) return; using StreamReader reader = new(recipeFile); string? line; while ((line = reader.ReadLine()) != null) { Match match = _recipeHeaderRegex.Match(line); if (match.Success) { // Store previous recipe. if ((currentRecipeColor != null) && testRecipe.IsValidForConcentration(concentration)) { SetRecipe(currentRecipeColor, testRecipe); } testRecipe.Clear(); currentRecipeColor = match.Groups["colorname"].Value; inRecipe = true; } else if (inRecipe) { match = _recipeIngredientRegex.Match(line); if (!match.Success) continue; string ingredient = match.Groups["ingredient"].Value; uint quantity = uint.Parse(match.Groups["quantity"].Value); testRecipe.AddReagent(ingredient, quantity); } } if (!inRecipe || (currentRecipeColor == null)) return; // Store final recipe. if (testRecipe.IsValidForConcentration(concentration)) { SetRecipe(currentRecipeColor, testRecipe); } } private void SaveRecipes(Dictionary recipeDict, string filename) { string recipeFile = Path.Combine(Directory, filename); using StreamWriter writer = new(recipeFile, false); foreach (KeyValuePair pair in recipeDict) { writer.WriteLine("--- Recipe: {0}", pair.Key); foreach (PaintRecipe.ReagentQuantity ingredient in pair.Value.Reagents) { writer.WriteLine("{0,-14} | {1}", ingredient.Name, ingredient.Quantity); } } } private void DeleteRecipes(Dictionary recipeDict, string filename) { string recipeFile = Path.Combine(Directory, filename); File.Delete(recipeFile); recipeDict.Clear(); } public void LoadRecipes() { LoadRecipes(Recipes, PaintRecipeFile, PaintRecipe.PaintRecipeMinConcentration); LoadRecipes(RibbonRecipes, RibbonRecipeFile, PaintRecipe.RibbonRecipeMinConcentration); } public void SaveRecipes() { SaveRecipes(Recipes, PaintRecipeFile); SaveRecipes(RibbonRecipes, RibbonRecipeFile); } public void ClearRecipes() { DeleteRecipes(Recipes, PaintRecipeFile); DeleteRecipes(RibbonRecipes, RibbonRecipeFile); } public void ClearPaintRecipes() { DeleteRecipes(Recipes, PaintRecipeFile); } public void ClearRibbonRecipes() { DeleteRecipes(RibbonRecipes, RibbonRecipeFile); } public void ExportWikiRecipes(string file) { StreamWriter writer = new(file); ExportWikiFormat(writer, Recipes); } public void ExportWikiRibbons(string file) { StreamWriter writer = new StreamWriter(file); ExportWikiFormat(writer, this.RibbonRecipes); } public void ExportWikiRecipes(TextWriter writer) { ExportWikiFormat(writer, this.Recipes); } public void ExportWikiRibbons(TextWriter writer) { ExportWikiFormat(writer, this.RibbonRecipes); } public static void ExportWikiFormat(TextWriter writer, Dictionary recipeDict) { using (writer) { writer.WriteLine("{| class='wikitable sortable' border=\"1\" style=\"background-color:#DEB887;\""); writer.WriteLine("! Color !! Recipe !! Missing Reactions? || Verified"); foreach (PaintColor color in PaletteService.Colors) { writer.WriteLine("|-"); string colorLine = "| "; colorLine += "style=\"font-weight: bold; background-color: #" + color.Red.ToString("X2") + color.Green.ToString("X2") + color.Blue.ToString("X2") + ";"; if (color.UseWhiteText) { // dark color gets light text colorLine += " color: #FFFFFF;"; } else { colorLine += "color: #000000;"; } colorLine += "\" | " + color.Name + " || "; if (recipeDict.TryGetValue(color.Name, out PaintRecipe? recipe)) { foreach (PaintRecipe.ReagentQuantity ingredient in recipe.Reagents) { colorLine += " " + ingredient; } } else { // no recipe } colorLine += " || "; if (recipe == null) { colorLine += "?"; } else if (recipe.HasMissingReactions()) { colorLine += "Y"; } else { colorLine += "N"; } colorLine += " || N"; writer.WriteLine(colorLine); } writer.WriteLine("|}"); } } public void ExportPaintMixRecipes(string file) { StreamWriter writer = new(file); ExportPaintMixFormat(writer, Recipes); } public void ExportPaintMixRibbons(string file) { StreamWriter writer = new StreamWriter(file); ExportPaintMixFormat(writer, this.RibbonRecipes); } public void ExportPaintMixRecipes(TextWriter writer) { ExportPaintMixFormat(writer, this.Recipes); } public void ExportPaintMixRibbons(TextWriter writer) { ExportWikiFormat(writer, this.RibbonRecipes); } public static void ExportPaintMixFormat(TextWriter writer, Dictionary recipeDict) { using (writer) { foreach (PaintColor color in PaletteService.Colors) { if (recipeDict.TryGetValue(color.Name, out PaintRecipe? recipe)) { if (recipe.Reagents.Count > 0) { string colorLine = $"{color.Name} :"; foreach (PaintRecipe.ReagentQuantity ingredient in recipe.Reagents) { Reagent reagent = ReagentService.GetReagent(ingredient.Name); colorLine += $" {reagent.PracticalPaintName} {ingredient.Quantity}"; } colorLine += $" - #{color.Red:X2}{color.Green:X2}{color.Blue:X2}"; writer.WriteLine(colorLine); } } else { // no recipe - skip } } } } public Reaction? FindReaction(Reagent? reagent1, Reagent? reagent2) { if ((reagent1 == null) || (reagent2 == null)) return null; return Reactions.Find(reagent1, reagent2); } public void SetReaction(Reagent reagent1, Reagent reagent2, Reaction reaction) { Reactions.Set(reagent1, reagent2, reaction); } public void ClearReaction(Reagent reagent1, Reagent reagent2) { Reactions.Remove(reagent1, reagent2); } public void SetRecipe(PaintRecipe recipe) { SetRecipe(PaletteService.FindNearest(recipe.ReactedColor), recipe); } public void SetRecipe(string colorName, PaintRecipe recipe) { if (Recipes.TryGetValue(colorName, out PaintRecipe? profileRecipe)) { profileRecipe.CopyFrom(recipe); } else { Recipes.Add(colorName, new PaintRecipe(recipe)); } } public void SetRibbonRecipe(PaintRecipe recipe) { SetRibbonRecipe(PaletteService.FindNearest(recipe.ReactedColor), recipe); } public void SetRibbonRecipe(string colorName, PaintRecipe recipe) { if (RibbonRecipes.TryGetValue(colorName, out PaintRecipe? profileRecipe)) { profileRecipe.CopyFrom(recipe); } else { RibbonRecipes.Add(colorName, new PaintRecipe(recipe)); } } } }