diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -5,4 +5,4 @@ obj/ *.swp *.zip .DS_Store -mac/build/DesertPaintLab.app +mac/bin diff --git a/RecipeGenerator.cs b/RecipeGenerator.cs --- a/RecipeGenerator.cs +++ b/RecipeGenerator.cs @@ -21,8 +21,10 @@ */ using System; +using System.IO; using System.Collections.Generic; using System.Collections.Concurrent; +using System.Text.RegularExpressions; using System.Threading; namespace DesertPaintLab @@ -55,10 +57,18 @@ namespace DesertPaintLab public class RecipeGenerator { - private class SearchNode + protected class SearchNode { //int initialReagentCount; List reagents; + public List Reagents + { + get + { + return reagents; + } + } + HashSet reagentInUse = new HashSet(); List costSortedReagents; PaintRecipe testRecipe = null; @@ -73,8 +83,13 @@ namespace DesertPaintLab testRecipe = value; } } + + public int InitialCount { get; private set; } public uint CurrentTargetQuantity { get; set; } public uint MaxQuantity { get; set; } + public uint UsedQuantity { get; private set; } + public uint CatalystCount { get; set; } + uint maxReagents; public uint MaxReagents { @@ -88,9 +103,6 @@ namespace DesertPaintLab currentWeights = new uint[maxReagents]; } } - public uint UsedQuantity { get; private set; } - - public uint CatalystCount { get; set; } uint[] currentWeights; public uint[] CurrentWeights @@ -125,13 +137,14 @@ namespace DesertPaintLab UsedQuantity = 0; } - public int InitialCount { get; private set; } - public List Reagents + public SearchNode(List costSortedReagents) { - get - { - return reagents; - } + this.costSortedReagents = costSortedReagents; + this.reagents = new List(); + this.reagents.Add(NextFreeReagent(0)); + InitialCount = 0; + MaxReagents = 1; + UsedQuantity = 0; } public Reagent Reagent(int idx) @@ -248,6 +261,131 @@ namespace DesertPaintLab currentWeights[idx] = quantity; UsedQuantity += quantity; } + + public void SaveState(StreamWriter writer) + { + writer.WriteLine("---SearchNode---"); + writer.WriteLine("MaxReagents: {0}", MaxReagents); + writer.WriteLine("Reagents: {0}", reagents.Count); + for (int i = 0; i < reagents.Count; ++i) + { + uint idx = reagents[i]; + uint weight = currentWeights[i]; + writer.WriteLine("Reagent: {0},{1},{2}", idx, reagentInUse.Contains(idx) ? 1 : 0, weight); + } + // pulled from parent: List costSortedReagents; + // new on construct: PaintRecipe testRecipe = null; + writer.WriteLine("CurrentTargetQuantity: {0}", CurrentTargetQuantity); + writer.WriteLine("MaxQuantity: {0}", MaxQuantity); + writer.WriteLine("UsedQuantity: {0}", UsedQuantity); + writer.WriteLine("CatalystCount: {0}", CatalystCount); + writer.WriteLine("InitialCount: {0}", InitialCount); + writer.WriteLine("---EndNode---"); + + } + + static Regex keyValueRegex = new Regex(@"(\w+)\:\s*(.*)\s*$"); + static Regex reagentPartsRegex = new Regex(@"(?\d+),(?\d+),(?\d+)"); + public bool LoadState(StreamReader reader) + { + string line = reader.ReadLine(); + if (!line.Equals("---SearchNode---")) + { + return false; + } + + bool success = true; + Match match; + int reagentIdx = 0; + while ((line = reader.ReadLine()) != null) + { + if (line.Equals("---EndNode---")) + { + break; + } + match = keyValueRegex.Match(line); + if (match.Success) + { + switch (match.Groups[1].Value) + { + case "Reagents": + { + int reagentCount = int.Parse(match.Groups[2].Value); + reagents = new List(reagentCount); + reagentInUse.Clear(); + reagentIdx = 0; + } + break; + case "Reagent": + { + Match reagentInfo = reagentPartsRegex.Match(match.Groups[2].Value); + if (reagentInfo.Success) + { + uint reagentId = uint.Parse(reagentInfo.Groups["id"].Value); + int isInUse = int.Parse(reagentInfo.Groups["inUse"].Value); + uint weight = uint.Parse(reagentInfo.Groups["weight"].Value); + reagents.Add(reagentId); + currentWeights[reagentIdx] = weight; + if (isInUse != 0) + { + reagentInUse.Add(reagentId); + } + } + else + { + success = false; + } + } + break; + case "CurrentTargetQuantity": + { + uint value = uint.Parse(match.Groups[2].Value); + CurrentTargetQuantity = value; + } + break; + case "MaxQuantity": + { + uint value = uint.Parse(match.Groups[2].Value); + MaxQuantity = value; + } + break; + case "MaxReagents": + { + uint value = uint.Parse(match.Groups[2].Value); + MaxReagents = value; + } + break; + case "UsedQuantity": + { + uint value = uint.Parse(match.Groups[2].Value); + UsedQuantity = value; + } + break; + case "CatalystCount": + { + uint value = uint.Parse(match.Groups[2].Value); + CatalystCount = value; + } + break; + case "InitialCount": + { + int value = int.Parse(match.Groups[2].Value); + InitialCount = value; + } + break; + default: + success = false; + break; + } + } + else + { + success = false; + break; + } + } + return success; + } } const uint DEFAULT_MAX_QUANTITY = 14; // minimum recipe: 10 base + 4 catalysts @@ -260,6 +398,8 @@ namespace DesertPaintLab ReactionSet reactions; bool running = false; + + int runningThreads = 0; SortedDictionary recipeCosts = new SortedDictionary(); SortedDictionary recipes = new SortedDictionary(); @@ -282,8 +422,9 @@ namespace DesertPaintLab public event EventHandler Progress; public event EventHandler NewRecipe; - public RecipeGenerator() + public RecipeGenerator(ReactionSet reactions) { + this.reactions = reactions; } public SortedDictionary Recipes @@ -302,6 +443,14 @@ namespace DesertPaintLab } } + public bool CanResume + { + get + { + return (!running && (searchQueue.Count > 0)); + } + } + private class ReagentCostSort : IComparer { public int Compare(Reagent reagent1, Reagent reagent2) @@ -310,36 +459,21 @@ namespace DesertPaintLab } } - public void InitRecipes(SortedDictionary recipes, ReactionSet reactions) + public void InitRecipes(SortedDictionary recipes) { if (running) { return; } - this.reactions = reactions; foreach (PaintRecipe recipe in recipes.Values) { - // TODO: copy? - AddCheapestRecipe(recipe); + PaintRecipe recipeCopy = new PaintRecipe(recipe); + AddCheapestRecipe(recipeCopy); } } - public void BeginRecipeGeneration(ReactionSet reactions, uint maxQuantity, uint maxReagents, uint fullQuantityDepth, uint fullQuantity) + private void InitSortedReagents() { - if (running) - { - // Already running - don't start again - return; - } - this.running = true; - - this.reactions = reactions; - //this.maxQuantity = maxQuantity; - this.maxReagents = maxReagents; - this.fullQuantity = fullQuantity; - this.fullQuantityDepth = fullQuantityDepth; - - // first, sort reagents by cost. costSortedReagents.Clear(); foreach (string name in ReagentManager.Names) { @@ -350,6 +484,23 @@ namespace DesertPaintLab } } costSortedReagents.Sort(new ReagentCostSort()); + } + + public void BeginRecipeGeneration(uint maxQuantity, uint maxReagents, uint fullQuantityDepth, uint fullQuantity) + { + if (running) + { + // Already running - don't start again + return; + } + + //this.maxQuantity = maxQuantity; + this.maxReagents = maxReagents; + this.fullQuantity = fullQuantity; + this.fullQuantityDepth = fullQuantityDepth; + + // first, sort reagents by cost. + InitSortedReagents(); this.maxReagents = (uint)Math.Min(costSortedReagents.Count, this.maxReagents); totalReagents = (uint)costSortedReagents.Count; @@ -368,6 +519,11 @@ namespace DesertPaintLab } } + while (!searchQueue.IsEmpty) + { + SearchNode node; + searchQueue.TryDequeue(out node); + } for (uint reagentIdx = 0; reagentIdx < costSortedReagents.Count; ++reagentIdx) { SearchNode initialNode = new SearchNode(costSortedReagents, reagentIdx); @@ -378,19 +534,7 @@ namespace DesertPaintLab // start worker threads to do the actual work - requestCancel = false; - running = true; - // Start the workers thread - for (int i = 0; i < costSortedReagents.Count; ++i) - { - Thread thr = new Thread(new ThreadStart(this.Generate)); - generatorThreads.Add(thr); - } - foreach (Thread thr in generatorThreads) - { - thr.Start(); - } - + ResumeRecipeGeneration(); } public void ResumeRecipeGeneration() @@ -400,10 +544,22 @@ namespace DesertPaintLab // Already running - don't start again return; } - this.running = true; + running = true; requestCancel = false; - for (int i = 0; i < costSortedReagents.Count; ++i) + //System.Console.WriteLine("Resuming recipe generation: pre-threads={0} reagent count={1} search queue={2}", runningThreads, costSortedReagents.Count, searchQueue.Count); + runningThreads = 0; // presumably! + + int threadCount = Math.Min(costSortedReagents.Count, searchQueue.Count); + if (threadCount == 0) + { + if (Finished != null) + { + Finished(this, null); + } + } + generatorThreads.Clear(); + for (int i = 0; i < threadCount; ++i) { Thread thr = new Thread(new ThreadStart(this.Generate)); generatorThreads.Add(thr); @@ -414,10 +570,147 @@ namespace DesertPaintLab } } + public bool SaveState(string file) + { + if (running) + { + // can't save state while running + return false; + } + + using (StreamWriter writer = new StreamWriter(file, false)) + { + writer.WriteLine("MaxReagents: {0}", maxReagents); + writer.WriteLine("FullQuantityDepth: {0}", fullQuantityDepth); + writer.WriteLine("FullQuantity: {0}", fullQuantity); + writer.WriteLine("TotalReagents: {0}", totalReagents); + writer.WriteLine("RecipeCount: {0}", recipeCount); + foreach (KeyValuePair pair in recipes) + { + PaintRecipe recipe = pair.Value; + string colorName = Palette.FindNearest(recipe.ReactedColor); + writer.WriteLine("BeginRecipe: {0}", colorName); + foreach (PaintRecipe.RecipeIngredient ingredient in recipe.Ingredients) + { + writer.WriteLine("Ingredient: {0}={1}", ingredient.name, ingredient.quantity); + } + writer.WriteLine("EndRecipe: {0}", colorName); + } + writer.WriteLine("SearchNodes: {0}", searchQueue.Count); + foreach (SearchNode node in searchQueue) + { + node.SaveState(writer); + } + } + return true; + } + + static Regex keyValueRegex = new Regex(@"(?\w+)\:\s*(?.+)\s*"); + static Regex ingredientRegex = new Regex(@"(?(\w+\s)*\w+)\s*=\s*(?\d)\s*"); + public bool LoadState(string file) + { + // cannot be running, and reactions must be set + if (running) + { + return false; + } + + InitSortedReagents(); + bool success = true; + + PaintRecipe currentRecipe = null; + Match match; + string line; + using (StreamReader reader = new StreamReader(file, false)) + { + while (success && ((line = reader.ReadLine()) != null)) + { + match = keyValueRegex.Match(line); + if (match.Success) + { + string value = match.Groups["value"].Value; + switch(match.Groups["key"].Value) + { + case "MaxReagents": + maxReagents = uint.Parse(value); + break; + case "FullQuantityDepth": + fullQuantityDepth = uint.Parse(value); + break; + case "FullQuantity": + fullQuantity = uint.Parse(value); + break; + case "TotalReagents": + totalReagents = uint.Parse(value); + break; + case "RecipeCount": + recipeCount = int.Parse(value); + break; + case "BeginRecipe": + currentRecipe = new PaintRecipe(); + currentRecipe.Reactions = reactions; + break; + case "EndRecipe": + if (currentRecipe != null) + { + PaintColor color = currentRecipe.ReactedColor; + uint cost = currentRecipe.Cost; + recipes[color.Name] = currentRecipe; // replace + recipeCosts[color.Name] = cost; + currentRecipe = null; + } + break; + case "Ingredient": + if (currentRecipe != null) + { + Match ingredientMatch = ingredientRegex.Match(match.Groups["value"].Value); + if (ingredientMatch.Success) + { + uint quantity = uint.Parse(ingredientMatch.Groups["quantity"].Value); + currentRecipe.AddReagent(ingredientMatch.Groups["ingredient"].Value, quantity); + } + else + { + success = false; + } + } + break; + case "SearchNodes": + int nodeCount = int.Parse(match.Groups["value"].Value); + for (int i = 0; i < nodeCount; ++i) + { + SearchNode node = new SearchNode(costSortedReagents); + success = success && node.LoadState(reader); + if (success) + { + searchQueue.Enqueue(node); + } + } + break; + default: + success = false; + break; + } + } + else + { + success = false; + break; + } + } + return success; + } + } + private void Generate() { SearchNode node; + lock(workerLock) + { + ++runningThreads; + } + bool ok = true; do { @@ -433,7 +726,7 @@ namespace DesertPaintLab node.InitForQuantity(targetQuantity); } while (targetQuantity > 10 && (node.CurrentTargetQuantity != node.UsedQuantity)); - while (ok = Iterate(node) && !requestCancel) + while ((ok = Iterate(node)) && !requestCancel) { if (Progress != null) { @@ -451,17 +744,19 @@ namespace DesertPaintLab bool done = false; lock(workerLock) { - generatorThreads.Remove(Thread.CurrentThread); + --runningThreads; + //generatorThreads.Remove(Thread.CurrentThread); - done = (generatorThreads.Count == 0); + done = (runningThreads == 0); } if (done) { + running = false; + requestCancel = false; if (Finished != null) { Finished(this, null); } - running = false; } } @@ -698,12 +993,13 @@ namespace DesertPaintLab public void Wait() { - if (running) + if (generatorThreads.Count > 0) { foreach (Thread thr in generatorThreads) { thr.Join(); } + generatorThreads.Clear(); } } diff --git a/RecipeGeneratorWindow.cs b/RecipeGeneratorWindow.cs --- a/RecipeGeneratorWindow.cs +++ b/RecipeGeneratorWindow.cs @@ -31,6 +31,11 @@ namespace DesertPaintLab PlayerProfile profile; bool canceling = false; bool running = false; + bool pauseForCheckpoint = false; + + const long RECIPE_SAVE_INTERVAL = 30000; // msec between saving recipes + const long CHECKPOINT_INTERVAL = 17000; // msec between saving out generator state + const string STATE_FILE = "dp_generator_state"; static Gtk.ListStore colorStore = new Gtk.ListStore(typeof(string)); @@ -38,6 +43,8 @@ namespace DesertPaintLab long lastProgressUpdate; long lastStatusUpdate; + long lastProfileSave; + long lastCheckpoint; static public Gtk.ListStore RecipeModel { @@ -67,6 +74,8 @@ namespace DesertPaintLab recipeList.AppendColumn(recipeColorColumn); recipeColorColumn.AddAttribute(recipeColumnCell, "text", 0); + colorStore.Clear(); + colorStore.SetSortColumnId(0, Gtk.SortType.Ascending); recipeList.Model = RecipeModel; @@ -80,9 +89,31 @@ namespace DesertPaintLab colorStore.AppendValues(key); } - countLabel.Text = String.Format("{0} / {1}", profile.Recipes.Count, Palette.Count); canceling = false; running = false; + pauseForCheckpoint = false; + + generator = new RecipeGenerator(profile.Reactions); + generator.InitRecipes(profile.Recipes); + + generator.Progress += OnProgress; + generator.Finished += OnFinished; + generator.NewRecipe += OnNewRecipe; + + string stateFile = System.IO.Path.Combine(profile.Directory, STATE_FILE); + if (System.IO.File.Exists(stateFile)) + { + generator.LoadState(stateFile); + if (generator.CanResume) + { + beginButton.Label = "Restart"; + stopResumeButton.Label = "Resume"; + stopResumeButton.Sensitive = true; + } + } + countLabel.Text = String.Format("{0} / {1}", generator.Recipes.Count, Palette.Count); + + Destroyed += OnDestroyed; } protected void OnMaxIngredientsChanged(object sender, EventArgs e) @@ -112,22 +143,16 @@ namespace DesertPaintLab protected void OnBegin(object sender, EventArgs e) { maxIngredientsSpinButton.Sensitive = false; - ExportAction.Sensitive = false; - SettingsAction.Sensitive = false; + ExportToWikiAction.Sensitive = false; + IngredientsAction.Sensitive = false; maxRecipeSpinButton.Sensitive = false; beginButton.Sensitive = false; // TODO: change to "pause"? stopResumeButton.Sensitive = true; fullQuantitySpinButton.Sensitive = false; fullQuantityDepthSpinButton.Sensitive = false; - generator = new RecipeGenerator(); - - generator.InitRecipes(profile.Recipes, profile.Reactions); countLabel.Text = String.Format("{0} / {1}", generator.Recipes.Count, Palette.Count); - generator.Progress += OnProgress; - generator.Finished += OnFinished; - generator.NewRecipe += OnNewRecipe; // TODO: hook up event notifications // - progress // - complete @@ -146,10 +171,15 @@ namespace DesertPaintLab lastProgressUpdate = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; lastStatusUpdate = lastProgressUpdate; + lastProfileSave = lastProgressUpdate; + lastCheckpoint = lastProgressUpdate; + running = true; - stopResumeButton.Label = "Stop"; + canceling = false; + pauseForCheckpoint = false; + stopResumeButton.Label = "Pause"; - generator.BeginRecipeGeneration(profile.Reactions, (uint)maxRecipeSpinButton.ValueAsInt, (uint)maxIngredientsSpinButton.ValueAsInt, (uint)fullQuantityDepthSpinButton.ValueAsInt, (uint)fullQuantitySpinButton.ValueAsInt); + generator.BeginRecipeGeneration((uint)maxRecipeSpinButton.ValueAsInt, (uint)maxIngredientsSpinButton.ValueAsInt, (uint)fullQuantityDepthSpinButton.ValueAsInt, (uint)fullQuantitySpinButton.ValueAsInt); } protected void OnStopResume(object sender, EventArgs e) @@ -159,19 +189,25 @@ namespace DesertPaintLab if (running) { canceling = true; + pauseForCheckpoint = false; generator.Stop(); } else { - // this must be a resume + // Resume previous run + ExportToWikiAction.Sensitive = false; + IngredientsAction.Sensitive = false; lastProgressUpdate = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; lastStatusUpdate = lastProgressUpdate; + lastProfileSave = lastProgressUpdate; + lastCheckpoint = lastProgressUpdate; canceling = false; + pauseForCheckpoint = false; running = true; - stopResumeButton.Label = "Stop"; - generator.BeginRecipeGeneration(profile.Reactions, (uint)maxRecipeSpinButton.ValueAsInt, (uint)maxIngredientsSpinButton.ValueAsInt, (uint)fullQuantityDepthSpinButton.ValueAsInt, (uint)fullQuantitySpinButton.ValueAsInt); + stopResumeButton.Label = "Pause"; + generator.ResumeRecipeGeneration(); } } } @@ -179,21 +215,37 @@ namespace DesertPaintLab protected void OnFinished(object sender, EventArgs args) { Gtk.Application.Invoke(delegate { - running = false; - beginButton.Sensitive = true; - ExportAction.Sensitive = true; - SettingsAction.Sensitive = true; - stopResumeButton.Sensitive = false; - maxIngredientsSpinButton.Sensitive = true; - maxRecipeSpinButton.Sensitive = true; - fullQuantitySpinButton.Sensitive = true; - fullQuantityDepthSpinButton.Sensitive = true; - //generator = null; // don't. Hang on to generator for resume. - profile.SaveRecipes(); - if (canceling) + generator.Wait(); + if (pauseForCheckpoint) + { + pauseForCheckpoint = false; + generator.SaveState(System.IO.Path.Combine(profile.Directory, STATE_FILE)); + generator.ResumeRecipeGeneration(); + } + else { - stopResumeButton.Label = "Resume"; - stopResumeButton.Sensitive = true; + running = false; + beginButton.Sensitive = true; + ExportToWikiAction.Sensitive = true; + IngredientsAction.Sensitive = true; + stopResumeButton.Sensitive = false; + maxIngredientsSpinButton.Sensitive = true; + maxRecipeSpinButton.Sensitive = true; + fullQuantitySpinButton.Sensitive = true; + fullQuantityDepthSpinButton.Sensitive = true; + //generator = null; // don't. Hang on to generator for resume. + profile.SaveRecipes(); + if (canceling) + { + generator.SaveState(System.IO.Path.Combine(profile.Directory, STATE_FILE)); + stopResumeButton.Label = "Resume"; + stopResumeButton.Sensitive = true; + beginButton.Label = "Restart"; + } + else + { + System.IO.File.Delete(System.IO.Path.Combine(profile.Directory, STATE_FILE)); + } } }); } @@ -228,6 +280,14 @@ namespace DesertPaintLab countLabel.Text = String.Format("{0} / {1}", generator.Recipes.Count, Palette.Count); } profile.SetRecipe(recipe); + + long progressTime = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; + long delta = progressTime - lastProfileSave; + if (delta >= RECIPE_SAVE_INTERVAL) + { + profile.SaveRecipes(); + lastProfileSave = progressTime; + } }); } @@ -251,6 +311,14 @@ namespace DesertPaintLab lastStatusUpdate = progressTime; } //progressBar.Fraction = (double)((generator.RecipeCount / 10000) % 100) / 100.0; + + delta = progressTime - lastCheckpoint; + if (delta > CHECKPOINT_INTERVAL) + { + pauseForCheckpoint = true; + lastCheckpoint = progressTime; + generator.Stop(); + } }); } @@ -306,6 +374,20 @@ namespace DesertPaintLab ReagentWindow win = new ReagentWindow(profile); win.Show(); } + + protected void OnDestroyed(object o, EventArgs args) + { + if (running) + { + // window closed while generator running: stop and save + generator.Finished -= OnFinished; + generator.Progress -= OnProgress; + generator.NewRecipe -= OnNewRecipe; + generator.Stop(); + generator.Wait(); + generator.SaveState(System.IO.Path.Combine(profile.Directory, STATE_FILE)); + } + } } } diff --git a/gtk-gui/gui.stetic b/gtk-gui/gui.stetic --- a/gtk-gui/gui.stetic +++ b/gtk-gui/gui.stetic @@ -6,7 +6,7 @@ - + diff --git a/mac/build-mac-bundle.sh b/mac/build-mac-bundle.sh --- a/mac/build-mac-bundle.sh +++ b/mac/build-mac-bundle.sh @@ -19,8 +19,8 @@ fi /bin/cp ../bin/Release/ingredients.txt bin/DesertPaintLab.app/Contents/Resources/ /bin/cp -r ../bin/Release/template bin/DesertPaintLab.app/Contents/Resources/template -/usr/bin/defaults write bin/DesertPaintLab.app/Info.plist CFBundleShortVersionString ${VERSION} -/usr/bin/defaults write bin/DesertPaintLab.app/Info.plist CFBundleVersion ${VERSION} +/usr/bin/defaults write `pwd`/bin/DesertPaintLab.app/Contents/Info.plist CFBundleShortVersionString ${VERSION} +/usr/bin/defaults write `pwd`/bin/DesertPaintLab.app/Contents/Info.plist CFBundleVersion ${VERSION} # package up into a DMG /usr/bin/hdiutil create -volname DesertPaintLab -srcfolder bin/ -ov -format UDIF -fs HFS+ DesertPaintLab.dmg