/* * Copyright (c) 2010, Tess Snider Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.IO; using System.Collections.Generic; using System.Text.RegularExpressions; using Gtk; using DesertPaintLab; public partial class MainWindow : Gtk.Window { const int colorTolerance = 2; int swatchHeight = 24; int colorBarWidth = 306; int redBarSpacing = 32; int greenBarSpacing = 42; int blueBarSpacing = 52; bool unsavedData = false; bool shouldShutDown = false; string appDataPath; List profileList = new List(); PlayerProfile profile = null; PaintColor expectedColor = new PaintColor(); PaintColor reactedColor = new PaintColor(); int screenWidth = 0; int screenHeight = 0; int pixelMultiplier = 1; bool enableDebugMenu = false; Gdk.Window rootWindow = null; Gdk.Pixbuf screenBuffer = null; Reagent reagent1 = null; Reagent reagent2 = null; Reagent reagent3 = null; public bool ShouldShutDown { get { return shouldShutDown; } } public MainWindow () : base(Gtk.WindowType.Toplevel) { appDataPath = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DesertPaintLab"); if (!System.IO.Directory.Exists(appDataPath)) { System.IO.Directory.CreateDirectory(appDataPath); } DirectoryInfo di = new DirectoryInfo(appDataPath); DirectoryInfo[] dirs = di.GetDirectories(); foreach (DirectoryInfo dir in dirs) { if (dir.Name != "template") { profileList.Add(dir.Name); } } Palette.Load(System.IO.Path.Combine( System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "colors.txt")); Build(); if (unmodifiedSwatch != null) { unmodifiedSwatch.Clear(); } if (reactionSwatch != null) { reactionSwatch.Clear(); } // get the root window rootWindow = Gdk.Global.DefaultRootWindow; // get its width and height rootWindow.GetSize(out screenWidth, out screenHeight); string settingsPath = System.IO.Path.Combine(appDataPath, "settings"); if (System.IO.File.Exists(settingsPath)) { LoadSettings(settingsPath); } this.DebugAction.Visible = enableDebugMenu; ScreenCheckDialog screenCheckDialog = new ScreenCheckDialog(); screenCheckDialog.ScreenWidth = screenWidth; screenCheckDialog.ScreenHeight = screenHeight; screenCheckDialog.GamePixelWidth = pixelMultiplier; ResponseType resp = (ResponseType)screenCheckDialog.Run(); screenWidth = screenCheckDialog.ScreenWidth; screenHeight = screenCheckDialog.ScreenHeight; pixelMultiplier = screenCheckDialog.GamePixelWidth; screenCheckDialog.Destroy(); SaveSettings(settingsPath); screenBuffer = new Gdk.Pixbuf(Gdk.Colorspace.Rgb, false, 8, screenWidth, screenHeight); swatchHeight *= pixelMultiplier; colorBarWidth *= pixelMultiplier; redBarSpacing *= pixelMultiplier; greenBarSpacing *= pixelMultiplier; blueBarSpacing *= pixelMultiplier; if (!OpenProfile()) { shouldShutDown = true; } } static Regex optionEntry = new Regex(@"(?[^#=][^=]*)=(?.*)$"); void LoadSettings(string file) { string line; Match match; using (StreamReader reader = new StreamReader(file)) { while ((line = reader.ReadLine()) != null) { match = optionEntry.Match(line); if (match.Success) { String optName = match.Groups["opt"].Value.ToLower(); String optVal = match.Groups["optval"].Value.Trim(); switch (optName) { case "screenwidth": try { int val = Int32.Parse(optVal); if (val > 0) { screenWidth = val; } } catch (FormatException) { // ignore } break; case "screenheight": try { int val = Int32.Parse(optVal); if (val > 0) { screenHeight = val; } } catch (FormatException) { // ignore } break; case "pixelmultiplier": try { int val = Int32.Parse(optVal); if (val > 0) { pixelMultiplier = val; } } catch (FormatException) { // ignore } break; case "debug": try { bool val = Boolean.Parse(optVal.ToLower()); enableDebugMenu = val; } catch (FormatException) { // ignore } break; default: // ignore break; } } } } } void SaveSettings(string file) { using (StreamWriter writer = new StreamWriter(file)) { writer.WriteLine("screenwidth={0}", screenWidth); writer.WriteLine("screenheight={0}", screenHeight); writer.WriteLine("pixelmultiplier={0}", pixelMultiplier); writer.WriteLine("debug={0}", enableDebugMenu); } } bool ConfirmedExit() { if (unsavedData) { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Warning, ButtonsType.OkCancel, "Your last reaction was unsaved." + "Are you sure you want to quit?"); ResponseType resp = (ResponseType)md.Run(); md.Destroy(); return (resp == ResponseType.Ok); } return true; } void SetProfileName(string name) { profile = new PlayerProfile(name, System.IO.Path.Combine(appDataPath, name)); statusBar.Push(0, name); } bool NewProfile() { bool newProfileCreated = false; bool duplicateName = false; NewProfileDialog newProfileDialog = new NewProfileDialog(); ResponseType resp = (ResponseType)newProfileDialog.Run(); if (resp == ResponseType.Ok) { // Make sure profile doesn't already exist. foreach (string profileName in profileList) { if (profileName == newProfileDialog.ProfileName) { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Error, ButtonsType.Ok, "That profile name already exists."); resp = (ResponseType)md.Run(); md.Destroy(); duplicateName = true; break; } } if (!duplicateName) { // Set profile name. SetProfileName(newProfileDialog.ProfileName); profile.Initialize(); newProfileCreated = true; } } newProfileDialog.Destroy(); return newProfileCreated; } bool OpenProfile() { bool profileSelected = false; if (profileList.Count > 0) { SelectProfileDialog selectProfileDialog = new SelectProfileDialog(); selectProfileDialog.ProfileList = profileList; ResponseType resp = (ResponseType)selectProfileDialog.Run(); selectProfileDialog.Destroy(); string selectedProfile = selectProfileDialog.SelectedProfile; if ((resp == ResponseType.Ok) && (selectedProfile.Length > 0)) // Selected a profile. { SetProfileName(selectedProfile); profileSelected = true; } else if (resp == ResponseType.Accept) // New profile. { profileSelected = NewProfile(); } } else { FirstRunDialog firstRunDialog = new FirstRunDialog(); ResponseType resp = (ResponseType)firstRunDialog.Run(); firstRunDialog.Destroy(); if (resp == ResponseType.Ok) // New profile { profileSelected = NewProfile(); } else if (resp == ResponseType.Accept) // Import { FileChooserDialog fileDialog = new FileChooserDialog("Select reactions.txt file.", this, FileChooserAction.Open, Gtk.Stock.Cancel, ResponseType.Cancel, Gtk.Stock.Open, ResponseType.Accept); resp = (ResponseType)fileDialog.Run(); if (resp == ResponseType.Accept) { string fileName = fileDialog.Filename; string directory = fileDialog.CurrentFolder; if (fileName == "reactions.txt") { profileSelected = NewProfile(); if (profileSelected) { profile.Import(directory); } } } } } if (profileSelected) { profile.Load(); PopulateDropDowns(); } return profileSelected; } void PopulateDropDowns() { ReagentManager.PopulateReagents(ref ingredient1ComboBox); ReagentManager.PopulateReagents(ref ingredient2ComboBox); ReagentManager.PopulateReagents(ref ingredient3ComboBox); ingredient2ComboBox.Sensitive = false; ingredient3ComboBox.Sensitive = false; Gtk.TreeIter iter; ingredient1ComboBox.Model.IterNthChild(out iter, 0); ingredient1ComboBox.SetActiveIter(iter); ingredient2ComboBox.Model.IterNthChild(out iter, 0); ingredient2ComboBox.SetActiveIter(iter); ingredient3ComboBox.Model.IterNthChild(out iter, 0); ingredient3ComboBox.SetActiveIter(iter); } protected void SetExpectedColor(byte red, byte green, byte blue) { expectedColor.Red = red; expectedColor.Green = green; expectedColor.Blue = blue; unmodifiedSwatch.Color = expectedColor; } protected void SetExpectedColor(PaintColor color) { SetExpectedColor(color.Red, color.Green, color.Blue); } protected void UpdateIngredients() { Reaction reaction1, reaction2; TreeIter selectIter; string reagentName; reagent1 = null; reagent2 = null; reagent3 = null; int expRedSum = 0; int expGreenSum = 0; int expBlueSum = 0; int reactRedSum = 0; int reactGreenSum = 0; int reactBlueSum = 0; bool reactionKnown = true; int pigmentCount = 0; saveButton.Sensitive = false; if (ingredient1ComboBox.GetActiveIter(out selectIter)) { reagentName = (string)ingredient1ComboBox.Model.GetValue(selectIter, 0); if ((reagentName == null) || (reagentName.Length == 0)) { ingredient2ComboBox.Sensitive = false; ingredient3ComboBox.Sensitive = false; unmodifiedSwatch.Clear(); captureButton.Sensitive = false; } else { reagent1 = ReagentManager.GetReagent(reagentName); ingredient2ComboBox.Sensitive = true; if (!reagent1.IsCatalyst) { expRedSum = reagent1.Color.Red; expGreenSum = reagent1.Color.Green; expBlueSum = reagent1.Color.Blue; pigmentCount = 1; } if (ingredient2ComboBox.GetActiveIter(out selectIter)) { reagentName = (string)ingredient2ComboBox.Model.GetValue(selectIter, 0); if ((reagentName == null) || (reagentName.Length == 0)) { ingredient3ComboBox.Sensitive = false; saveButton.Sensitive = false; reactionKnown = false; } else { reagent2 = ReagentManager.GetReagent(reagentName); ingredient3ComboBox.Sensitive = true; captureButton.Sensitive = true; if (!reagent2.IsCatalyst) { expRedSum += reagent2.Color.Red; expGreenSum += reagent2.Color.Green; expBlueSum += reagent2.Color.Blue; pigmentCount++; } reaction1 = profile.FindReaction(reagent1, reagent2); if (reaction1 != null) { ingredient3ComboBox.Sensitive = true; reactRedSum = reaction1.Red; reactGreenSum = reaction1.Green; reactBlueSum = reaction1.Blue;; } else { reactionKnown = false; ingredient3ComboBox.Sensitive = false; } if (ingredient3ComboBox.GetActiveIter(out selectIter)) { reagentName = (string)ingredient3ComboBox.Model.GetValue(selectIter, 0); if ((reagentName != null) && (reagentName.Length != 0)) { reagent3 = ReagentManager.GetReagent(reagentName); if (!reactionKnown) { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Error, ButtonsType.Ok, "To do a three-ingredient reaction test, " + "you must first recored the reaction of " + "the first two ingredients."); md.Run(); md.Destroy(); captureButton.Sensitive = false; } if (!reagent3.IsCatalyst) { expRedSum += reagent3.Color.Red; expGreenSum += reagent3.Color.Green; expBlueSum += reagent3.Color.Blue; pigmentCount++; } reaction1 = profile.FindReaction(reagent1, reagent3); reaction2 = profile.FindReaction(reagent2, reagent3); if (reactionKnown && (reaction1 == null) && (reaction2 == null)) { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Error, ButtonsType.Ok, "To do a three-ingredient reaction test, " + "you must first record the reaction of " + "either the first or second ingredient " + "with the third ingredient."); md.Run(); md.Destroy(); captureButton.Sensitive = false; } if (reaction1 != null) { reactRedSum += reaction1.Red; reactGreenSum += reaction1.Green; reactBlueSum += reaction1.Blue; } else { reactionKnown = false; } if (reaction2 != null) { reactRedSum += reaction2.Red; reactGreenSum += reaction2.Green; reactBlueSum += reaction2.Blue; } else { reactionKnown = false; } } } } } SetExpectedColor((byte)Math.Round((float)expRedSum / (float)pigmentCount), (byte)Math.Round((float)expGreenSum / (float)pigmentCount), (byte)Math.Round((float)expBlueSum / (float)pigmentCount)); if (reactionKnown) { reactedColor.Red = (byte)Math.Min(255, Math.Max(0, expectedColor.Red + reactRedSum)); reactedColor.Green = (byte)Math.Min(255, Math.Max(0, expectedColor.Green + reactGreenSum)); reactedColor.Blue = (byte)Math.Min(255, Math.Max(0, expectedColor.Blue + reactBlueSum)); reactionSwatch.Color = reactedColor; } else { reactionSwatch.Clear(); } } } } protected void OnDeleteEvent(object sender, DeleteEventArgs a) { if (ConfirmedExit()) { a.RetVal = true; Application.Quit(); } else { a.RetVal = false; } } bool IsPapyTexture(byte r, byte g, byte b) { return ((r > 0xD0) && (g > 0xC8) && (b > 0xA0)) && ((r < 0xF4) && (g < 0xE0) && (b < 0xC4)); } unsafe bool CaptureReactionColor() { // Take a screenshot. byte r, g, b; int pixelStart, otherPixelStart; bool colorMatch; Gdk.Image rootImage = rootWindow.GetImage(0, 0, screenWidth, screenHeight); screenBuffer.GetFromImage(rootImage, rootImage.Colormap, 0, 0, 0, 0, screenWidth, screenHeight); //screenBuffer.GetFromDrawable(rootWindow, // rootWindow.Colormap, 0, 0, 0, 0, screenWidth, screenHeight); int stride = screenBuffer.Rowstride; byte* pixBytes = (byte*)screenBuffer.Pixels; for (int x = 0; x < screenWidth - colorBarWidth; ++x) { for (int y = 0; y < (screenHeight - 53); ++y) { // Look for the color swatch. pixelStart = (y * stride) + (x * 3); r = pixBytes[pixelStart]; g = pixBytes[pixelStart + 1]; b = pixBytes[pixelStart + 2]; // 1.) Check if this is a dark pixel. if ((r < 0x46) && (g < 0x46) && (b < 0x46)) { // 2.) Check the pixel above it, // to see if it's from the papy texture. otherPixelStart = pixelStart - stride; if ((otherPixelStart >= 0) && IsPapyTexture(pixBytes[otherPixelStart++], pixBytes[otherPixelStart++], pixBytes[otherPixelStart])) { // 3.) Check the pixel below where the swatch should be, // to see if it's also from the papy texture. otherPixelStart = pixelStart + (stride * swatchHeight); if (IsPapyTexture(pixBytes[otherPixelStart++], pixBytes[otherPixelStart++], pixBytes[otherPixelStart])) { // pixBytes[pixelStart] = 0xFF; // pixBytes[pixelStart + 1] = 0x00; // pixBytes[pixelStart + 2] = 0xFF; // 4.) Scan the left border of the potential swatch // location. colorMatch = true; for (int i = 1; i < swatchHeight - 2; ++i) { otherPixelStart = pixelStart + (stride * i); if ((Math.Abs(r - pixBytes[otherPixelStart++]) > colorTolerance) || (Math.Abs(g - pixBytes[otherPixelStart++]) > colorTolerance) || (Math.Abs(b - pixBytes[otherPixelStart]) > colorTolerance)) { colorMatch = false; break; } } if (colorMatch) { // WE FOUND THE SWATCH! // Now we know where the color bars are. otherPixelStart = pixelStart + (redBarSpacing * stride); int pixelCount = 0; while ((pixBytes[otherPixelStart] > 0x9F) && (pixBytes[otherPixelStart + 1] < 0x62) && (pixBytes[otherPixelStart + 2] < 0x62)) { pixelCount++; // pixBytes[otherPixelStart] = 0x00; // pixBytes[otherPixelStart + 1] = 0xFF; // pixBytes[otherPixelStart + 2] = 0xFF; otherPixelStart += 3; } reactedColor.Red = (byte)Math.Round((float)pixelCount * 255f / (float)colorBarWidth); otherPixelStart = pixelStart + (greenBarSpacing * stride); pixelCount = 0; while ((pixBytes[otherPixelStart] < 0x62) && (pixBytes[otherPixelStart + 1] > 0x9F) && (pixBytes[otherPixelStart + 2] < 0x62)) { pixelCount++; // pixBytes[otherPixelStart] = 0x00; // pixBytes[otherPixelStart + 1] = 0xFF; // pixBytes[otherPixelStart + 2] = 0xFF; otherPixelStart += 3; } reactedColor.Green = (byte)Math.Round((float)pixelCount * 255f / (float)colorBarWidth); otherPixelStart = pixelStart + (blueBarSpacing * stride); pixelCount = 0; while ((pixBytes[otherPixelStart] < 0x62) && (pixBytes[otherPixelStart + 1] < 0x62) && (pixBytes[otherPixelStart + 2] > 0x9F)) { pixelCount++; // pixBytes[otherPixelStart] = 0x00; // pixBytes[otherPixelStart + 1] = 0xFF; // pixBytes[otherPixelStart + 2] = 0xFF; otherPixelStart += 3; } reactedColor.Blue = (byte)Math.Round((float)pixelCount * 255f / (float)colorBarWidth); // write out the screenshot //screenBuffer.Save("screenshot.png", "png"); return true; } } } } } } //screenBuffer.Save("screenshot.png", "png"); return false; } protected virtual void OnDebugScreenshot(object sender, System.EventArgs e) { Gdk.Image rootImage = rootWindow.GetImage(0, 0, screenWidth, screenHeight); screenBuffer.GetFromImage(rootImage, rootImage.Colormap, 0, 0, 0, 0, screenWidth, screenHeight); string screenshotDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); string filename; int i = 0; do { ++i; filename = System.IO.Path.Combine(screenshotDir, String.Format("DesertPaintLab_{0}.png", i)); } while (System.IO.File.Exists(filename)); screenBuffer.Save(filename, "png"); } protected virtual void OnCaptureButton(object sender, System.EventArgs e) { if (CaptureReactionColor()) { string warning = ""; if (reactedColor.Red == 0) { warning = warning + "\nRed is too low."; } if (reactedColor.Green == 0) { warning = warning + "\nGreen is too low."; } if (reactedColor.Blue == 0) { warning = warning + "\nBlue is too low."; } if (reactedColor.Red == 255) { warning = warning + "\nRed is too high."; } if (reactedColor.Green == 255) { warning = warning + "\nGreen is too high."; } if (reactedColor.Blue == 255) { warning = warning + "\nBlue is too high."; } if (warning.Length != 0) { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Error, ButtonsType.Ok, "Reaction clipped. You will need to do a " + "3-way reaction to test this pair. Details: " + warning); md.Run(); md.Destroy(); } else { this.reactionSwatch.Color = reactedColor; saveButton.Sensitive = true; } } else { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Error, ButtonsType.Ok, "Pigment Lab dialog box NOT FOUND. Please ensure " + "that there is an unobstructed view of the dialog " + "when you press the Capture button."); md.Run(); md.Destroy(); } } protected virtual void OnSaveButton(object sender, System.EventArgs e) { int r, g, b; if (reagent3 != null) { // A 3-reagent reaction. Reaction reaction1 = profile.FindReaction(reagent1, reagent2); Reaction reaction2 = profile.FindReaction(reagent1, reagent3); Reaction reaction3 = profile.FindReaction(reagent2, reagent3); r = reactedColor.Red - expectedColor.Red; g = reactedColor.Green - expectedColor.Green; b = reactedColor.Blue - expectedColor.Blue; if (reaction2 == null) { r = r - reaction1.Red - reaction3.Red; g = g - reaction1.Green - reaction3.Green; b = b - reaction1.Blue - reaction3.Blue; profile.SetReaction(reagent1, reagent3, new Reaction(r, g, b)); profile.Save(); saveButton.Sensitive = false; } else if (reaction3 == null) { r = r - reaction1.Red - reaction2.Red; g = g - reaction1.Green - reaction2.Green; b = b - reaction1.Blue - reaction2.Blue; profile.SetReaction(reagent2, reagent3, new Reaction(r, g, b)); profile.Save(); saveButton.Sensitive = false; } } else if ((reagent1 != null) && (reagent2 != null)) { // A 2-reagent reaction. r = reactedColor.Red - expectedColor.Red; g = reactedColor.Green - expectedColor.Green; b = reactedColor.Blue - expectedColor.Blue; profile.SetReaction(reagent1, reagent2, new Reaction(r, g, b)); profile.Save(); saveButton.Sensitive = false; } } protected virtual void OnChangedIngredient1(object sender, System.EventArgs e) { UpdateIngredients(); } protected virtual void OnChangedIngredient2(object sender, System.EventArgs e) { UpdateIngredients(); } protected virtual void OnChangedIngredient3(object sender, System.EventArgs e) { UpdateIngredients(); } protected virtual void OnNewProfile(object sender, System.EventArgs e) { if (unsavedData) { MessageDialog md = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Warning, ButtonsType.OkCancel, "Your last reaction was unsaved." + "Are you sure you want to lose your changes?"); ResponseType resp = (ResponseType)md.Run(); md.Destroy(); if (resp != ResponseType.Ok) { return; } } if (NewProfile()) { profile.Load(); PopulateDropDowns(); } } protected virtual void OnOpenProfile(object sender, System.EventArgs e) { bool profileSelected = false; SelectProfileDialog selectProfileDialog = new SelectProfileDialog(); selectProfileDialog.ProfileList = profileList; ResponseType resp = (ResponseType)selectProfileDialog.Run(); selectProfileDialog.Destroy(); if (resp == ResponseType.Ok) // Selected a profile. { SetProfileName(selectProfileDialog.SelectedProfile); profileSelected = true; } else if (resp == ResponseType.Accept) // New profile. { profileSelected = NewProfile(); } if (profileSelected) { profile.Load(); PopulateDropDowns(); } } protected virtual void OnAbout(object sender, System.EventArgs e) { AboutDialog aboutDialog = new AboutDialog(); aboutDialog.Run(); aboutDialog.Destroy(); } protected virtual void OnMenuExit (object sender, System.EventArgs e) { if (ConfirmedExit()) { Application.Quit(); } } protected virtual void OnExport(object sender, System.EventArgs e) { FileChooserDialog fileDialog = new FileChooserDialog("Select destination file.", this, FileChooserAction.Save, Gtk.Stock.Cancel, ResponseType.Cancel, Gtk.Stock.Save, ResponseType.Accept); ResponseType resp = (ResponseType)fileDialog.Run(); if (resp == ResponseType.Accept) { string fileName = fileDialog.Filename; string directory = fileDialog.CurrentFolder; profile.Export(System.IO.Path.Combine(directory, fileName)); } fileDialog.Destroy(); } protected virtual void RunSimulator(object sender, System.EventArgs e) { SimulatorWindow win = new SimulatorWindow(profile); win.Show(); } }