Changeset - 40eaee10ae56
[Not reviewed]
default
! ! !
Tess Snider (Malkyne) - 11 months ago 2021-07-04 10:58:53
this@malkyne.org
First commit. New UI.
58 files changed:
.hgeol
7
App.axaml
122
README.md
147
Changeset was too big and was cut off... Show full diff anyway
0 comments (0 inline, 0 general)
.gitignore
Show inline comments
 
new file 100644
 
.idea/
 
.vscode/
 
.vs/
 

	
 
bin/
 
obj/
 

	
 
*.user
...
 
\ No newline at end of file
.hgeol
Show inline comments
 
new file 100644
 
[repository]
 
native = LF
 

 
[patterns]
 
** = native
 
**.vcproj = CRLF
 

.hgignore
Show inline comments
 
new file 100644
 
syntax: glob
 

 
.idea/
 
.vscode/
 
.vs/
 

 
bin/
 
obj/
 

 
*.user
 

 
mac/old/
 
*.userprefs
 
*.orig
 
*.swp
 
*.zip
 
*.dmg
 
.DS_Store
 
mac/bin
App.axaml
Show inline comments
 
new file 100644
 
<Application xmlns="https://github.com/avaloniaui"
 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 
             xmlns:local="using:DesertPaintCodex"
 
             xmlns:converters="clr-namespace:DesertPaintCodex.Converters"
 
             x:Class="DesertPaintCodex.App">
 
    <Application.DataTemplates>
 
        <local:ViewLocator/>
 
    </Application.DataTemplates>
 

	
 
    <Application.Resources>
 
        <!-- Converters -->
 
        <converters:PaintToBrushConverter   x:Key="paintToBrush" />
 
        <converters:EnumBooleanConverter    x:Key="enumBoolean" />
 
        <converters:NotEnumBooleanConverter x:Key="notEnumBoolean" />
 
        
 
        <!-- Icons and Art -->
 
        <PathGeometry x:Key="Icon_AddProfile">M28 6a6 6 0 1 1-6-6A6 6 0 0 1 28 6Zm2 18s0-10-8-10-8 10-8 10ZM12 10H8V6H4v4H0v4H4v4H8V14h4Z</PathGeometry>
 
        <PathGeometry x:Key="Icon_UpArrow">M 16 8 8 0 0 8 h 5 v 8 H 11 V 8 Z</PathGeometry>
 
        <PathGeometry x:Key="Icon_DownArrow">M 16 8 8 16 0 8 H 5 V 0 H 11 v 8 z</PathGeometry>
 
        <PathGeometry x:Key="Icon_Catalyst">M 9 8 A 1 1 0 0 1 8 9 1 1 0 0 1 7 8 1 1 0 0 1 8 7 1 1 0 0 1 9 8 Z m 5.62 8 C 12.62 16 8.88 13.22 5.83 10.17 2.15 6.5 -1.12 1.87 0.37 0.37 v 0 c 1.5 -1.49 6.13 1.79 9.8 5.46 3.67 3.67 7 8.3 5.46 9.8 A 1.38 1.38 0 0 1 14.62 16 Z M 1.08 1.08 c -0.5 0.5 1 3.91 5.46 8.38 4.46 4.47 7.88 6 8.38 5.46 0.5 -0.54 -1 -3.91 -5.46 -8.38 C 5 2.07 1.58 0.58 1.08 1.08 Z M 1.38 16 a 1.4 1.4 0 0 1 -1 -0.37 c -1.49 -1.5 1.78 -6.13 5.46 -9.8 3.68 -3.67 8.3 -6.95 9.8 -5.46 v 0 c 1.49 1.5 -1.78 6.13 -5.46 9.8 C 7.12 13.22 3.41 16 1.38 16 Z M 14.61 1 c -1 0 -4.14 1.61 -8.07 5.55 -4.48 4.47 -6 7.88 -5.46 8.38 0.54 0.5 3.91 -1 8.38 -5.46 4.47 -4.46 6 -7.88 5.46 -8.38 A 0.43 0.43 0 0 0 14.61 1 Z</PathGeometry>
 
        <DrawingImage x:Key="Image_Splash">
 
            <DrawingGroup>
 
                <GeometryDrawing Brush="#5E443F" Geometry="M 0 0 H 256 V 256 H 0 Z"/>
 
                <GeometryDrawing Brush="#9D9F37" Geometry="M192 136c-11.31-11.31 0-16 0-16s-16 0-16 16v16H160V120a32 32 0 0 0-64 0V256h64V184h32C208 184 208 152 192 136Z"/>
 
                <GeometryDrawing Brush="#E1AD56" Geometry="m 112 8 c 0 0 -48 32 -48 48 0 16 16 32 32 32 h 16 C 112 88 96 72 96 56 96 40 112 8 112 8 Z"/>
 
                <GeometryDrawing Brush="#CA7091" Geometry="M128 88s-16-16-16-32S128 8 128 8s16 32 16 48S128 88 128 88Z"/>
 
                <GeometryDrawing Brush="#80ABA4" Geometry="m 144 8 c 0 0 48 32 48 48 0 16 -16 32 -32 32 h -16 c 0 0 16 -16 16 -32 C 160 40 144 8 144 8 Z"/>
 
            </DrawingGroup>
 
        </DrawingImage>
 
    </Application.Resources>
 
    
 
    <Application.Styles>
 
        <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
 
        <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseDark.xaml"/>
 
        <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
 

	
 
        <Style Selector="Window">
 
            <Setter Property="Foreground" Value="#FFEDD4"/>
 
            <Setter Property="Background" Value="#312E2F"/>
 
            <Style.Resources>
 
                <SolidColorBrush x:Key="FlatBackgroundBrush" Color="#312E2F" />
 
                <SolidColorBrush x:Key="GutterBackgroundBrush" Color="#282627" />
 
            </Style.Resources>
 
        </Style>
 
        
 
        <Style Selector="TextBlock">
 
            <Setter Property="FontSize" Value="14"/>
 
        </Style>
 
        
 
        <Style Selector="TextBlock.BlockHeader">
 
            <Setter Property="FontSize" Value="16"/>
 
            <Setter Property="FontWeight" Value="Bold"/>
 
        </Style>
 

	
 
        <Style Selector=":is(Panel).Activity">
 
            <Setter Property="Margin" Value="15"/>
 
        </Style>
 
        
 
        <Style Selector="Border.ThinFrame">
 
            <Setter Property="BorderBrush" Value="#81776B"/>
 
            <Setter Property="BorderThickness" Value="1"/>
 
            <Setter Property="CornerRadius" Value="3"/>
 
            <Setter Property="Padding" Value="8"/>
 
        </Style>
 
        
 
        <Style Selector="Border.Help">
 
            <Setter Property="Background" Value="#515E2E"/>
 
            <Setter Property="BorderThickness" Value="2"/>
 
            <Setter Property="BorderBrush" Value="#FFEDD4"/>
 
            <Setter Property="Width" Value="24"/>
 
            <Setter Property="Height" Value="24"/>
 
        </Style>
 
        
 
        <Style Selector="Border.Help > TextBlock">
 
            <Setter Property="Foreground" Value="#FFEDD4"/>
 
            <Setter Property="FontWeight" Value="Black"/>
 
            <Setter Property="FontSize" Value="14"/>
 
            <Setter Property="HorizontalAlignment" Value="Center"/>
 
        </Style>
 
                
 
        <Style Selector="Path.AddProfile">
 
            <Setter Property="Data" Value="{StaticResource Icon_AddProfile}"/>
 
            <Setter Property="Fill" Value="#E1AD56"/>
 
        </Style>
 
        
 
        <Style Selector="Path.Catalyst">
 
            <Setter Property="Data" Value="{StaticResource Icon_Catalyst}"/>
 
            <Setter Property="Fill" Value="#FFEDD4"/>
 
        </Style>
 
        
 
        <Style Selector="Path.UpArrow">
 
            <Setter Property="Data" Value="{StaticResource Icon_UpArrow}"/>
 
        </Style>
 
        
 
        <Style Selector="Path.DownArrow">
 
            <Setter Property="Data" Value="{StaticResource Icon_DownArrow}"/>
 
        </Style>
 
        
 
        <Style Selector="Path.Red">
 
            <Setter Property="Fill" Value="#CA7091"/>
 
        </Style>
 
        
 
        <Style Selector="Path.Green">
 
            <Setter Property="Fill" Value="#9D9F37"/>
 
        </Style>
 
        
 
        <Style Selector="Path.Blue">
 
            <Setter Property="Fill" Value="#80ABA4"/>
 
        </Style>
 
        
 
        <Style Selector="Border.ReagentSwatch">
 
            <Setter Property="BorderBrush" Value="{DynamicResource GutterBackgroundBrush}"/>
 
            <Setter Property="BorderThickness" Value="0"/>
 
            <Setter Property="CornerRadius" Value="3"/>
 
            <Setter Property="Padding" Value="8"/>
 
            <Setter Property="Width" Value="30"/>
 
            <Setter Property="Height" Value="30"/>
 
        </Style>
 
        
 
    </Application.Styles>
 
    
 
</Application>
App.axaml.cs
Show inline comments
 
new file 100644
 
using System;
 
using Avalonia;
 
using Avalonia.Controls;
 
using Avalonia.Controls.ApplicationLifetimes;
 
using Avalonia.Markup.Xaml;
 
using DesertPaintCodex.Services;
 
using DesertPaintCodex.Views;
 

	
 
namespace DesertPaintCodex
 
{
 
    internal class App : Application
 
    {
 
        private WelcomeView? _welcomeView;
 
        private MainWindow? _mainWindow;
 
        
 
        public override void Initialize()
 
        {
 
            AvaloniaXamlLoader.Load(this);
 
        }
 

	
 
        public override void OnFrameworkInitializationCompleted()
 
        { 
 
            ShowWelcomeView();
 
            base.OnFrameworkInitializationCompleted();
 
        }
 

	
 
        public void ReturnToWelcome()
 
        {
 
            ShowWelcomeView();
 
            CloseMainWindow();
 
        }
 

	
 
        private void ShowWelcomeView()
 
        {
 
            if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return;
 
            
 
            desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
 
            _welcomeView         = new WelcomeView();
 
            _welcomeView.Closed += OnWelcomeViewClosed;
 
            _welcomeView.Show();
 
        }
 

	
 
        private void ShowMainWindow()
 
        {
 
            if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return;
 
            
 
            desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
 
            _mainWindow          = new MainWindow();
 
            _mainWindow.Closing += _mainWindow.OnMainWindowClosing;
 
            desktop.MainWindow   = _mainWindow;
 
            desktop.MainWindow.Show();
 
        }
 

	
 
        private void Shutdown()
 
        {
 
            if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return;
 
            desktop.Shutdown();
 
        }
 

	
 
        private void CloseMainWindow()
 
        {
 
            if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return;
 
            if (_mainWindow != null)
 
            {
 
                desktop.MainWindow.Closing -= _mainWindow.OnMainWindowClosing;
 
            }
 
            desktop.MainWindow.Close();
 
            _mainWindow = null;
 
            desktop.MainWindow = null;
 
        }
 

	
 
        private void OnWelcomeViewClosed(object? obj, EventArgs args)
 
        {
 
            if (_welcomeView != null) _welcomeView.Closed -= OnWelcomeViewClosed;
 
            _welcomeView = null;
 

	
 
            if (ProfileManager.CurrentProfile == null)
 
            {
 
                Shutdown();
 
            }
 
            else
 
            {
 
                ShowMainWindow();
 
            }
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Assets/desert_paint_codex_icon.ico
Show inline comments
 
new file 100644
 
binary diff not shown
Converters/EnumBooleanConverter.cs
Show inline comments
 
new file 100644
 

	
 
using System;
 
using Avalonia.Data.Converters;
 

	
 
namespace DesertPaintCodex.Converters
 
{
 
    public class EnumBooleanConverter : IValueConverter
 
    {
 
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 
        {
 
            if (value is not Enum en) return false;
 
            
 
            string? parameterString = parameter.ToString();
 
            if (parameterString == null) return false;
 
            
 
            object parameterValue = Enum.Parse(value.GetType(), parameterString);
 
            return parameterValue.Equals(value);
 
        }
 

	
 
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 
        {
 
            if (value is not bool) return false;
 
            
 
            string? parameterString = parameter.ToString();
 
            if (parameterString == null) return false;
 

	
 
            return Enum.Parse(targetType, parameterString);
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Converters/NotEnumBooleanConverter.cs
Show inline comments
 
new file 100644
 
using System;
 

	
 
namespace DesertPaintCodex.Converters
 
{
 
    public class NotEnumBooleanConverter
 
    {
 
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 
        {
 
            if (value is not Enum en) return false;
 
            
 
            string? parameterString = parameter.ToString();
 
            if (parameterString == null) return false;
 
            
 
            object parameterValue = Enum.Parse(value.GetType(), parameterString);
 
            return !parameterValue.Equals(value);
 
        }
 

	
 
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 
        {
 
            throw new InvalidOperationException("NotEnumBooleanoConverter can only be used OneWay.");
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Converters/PaintToBrushConverter.cs
Show inline comments
 
new file 100644
 
using System;
 
using System.Collections.Generic;
 
using System.Globalization;
 
using Avalonia.Data.Converters;
 
using Avalonia.Media;
 
using DesertPaintCodex.Models;
 

	
 
namespace DesertPaintCodex.Converters
 
{
 
    /// <summary>
 
    /// Converts PaintColor to a SolidColorBrush.
 
    /// </summary>
 
    public class PaintToBrushConverter : IValueConverter
 
    {
 
        private static readonly SolidColorBrush NoColor = new();
 
        private static readonly Dictionary<PaintColor, SolidColorBrush> BrushPool = new();
 

	
 
        /// <inheritdoc/>
 
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
 
        {
 
            if (value is not PaintColor paintColor) return NoColor;
 
            
 
            if (BrushPool.TryGetValue(paintColor, out SolidColorBrush? brush)) return brush;
 
            
 
            brush = new SolidColorBrush(new Color(0xFF, paintColor.Red, paintColor.Green, paintColor.Blue));
 
            BrushPool.Add(paintColor, brush);
 

	
 
            return brush;
 
        }
 

	
 
        /// <inheritdoc/>
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
 
        {
 
            if (value is not SolidColorBrush brush) return new PaintColor(0, 0, 0);
 
            Color color = brush.Color;
 
            return new PaintColor(color.R, color.G, color.B);
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Data/colors.txt
Show inline comments
 
new file 100644
 
#F0F8FF	AliceBlue
 
#9966CC	Amethyst
 
#FAEBD7	AntiqueWhite
 
#00FFFF	Aqua
 
#7FFFD4	Aquamarine
 
#F0FFFF	Azure
 
#FF91AF	BakerMillerPink
 
#E3CF57	Banana
 
#7C0A02	BarnRed
 
#8E388E	Beet
 
#F5F5DC	Beige
 
#FFE4C4	Bisque
 
#010101	Black
 
#FFEBCD	BlanchedAlmond
 
#FF6700	BlazeOrange
 
#0000FF	Blue
 
#8A2BE2	BlueViolet
 
#873260	Boysenberry
 
#FF007F	BrightPink
 
#A52A2A	Brown
 
#800020	BurgundyRed
 
#DEB887	BurlyWood
 
#8A360F	BurntSienna
 
#8A3324	BurntUmber
 
#5F9EA0	CadetBlue
 
#FF6103	CadmiumOrange
 
#FF9912	CadmiumYellow
 
#E07020	Carrot
 
#7FFF00	Chartreuse
 
#D2691E	Chocolate
 
#3D59AB	CobaltBlue
 
#3D9140	CobaltGreen
 
#FF7F50	Coral
 
#6495ED	CornflowerBlue
 
#FFF8DC	Cornsilk
 
#DC143C	Crimson
 
#00008B	DarkBlue
 
#008B8B	DarkCyan
 
#B8860B	DarkGoldenRod
 
#006400	DarkGreen
 
#A9A9A9	DarkGrey
 
#1A2421	DarkJungleGreen
 
#BDB76B	DarkKhaki
 
#8B008B	DarkMagenta
 
#556B2F	DarkOliveGreen
 
#FF8C00	DarkOrange
 
#9932CC	DarkOrchid
 
#8B0000	DarkRed
 
#E9967A	DarkSalmon
 
#560319	DarkScarlet
 
#8FBC8F	DarkSeaGreen
 
#3C1414	DarkSienna
 
#483D8B	DarkSlateBlue
 
#2F4F4F	DarkSlateGrey
 
#00CED1	DarkTurquoise
 
#9400D3	DarkViolet
 
#FF1493	DeepPink
 
#00BFFF	DeepSkyBlue
 
#696969	DimGrey
 
#1E90FF	DodgerBlue
 
#00009C	DukeBlue
 
#FCE6C9	EggshellWhite
 
#00C957	EmeraldGreen
 
#D19275	Feldspar
 
#B22222	FireBrick
 
#FFFAF0	FloralWhite
 
#228B22	ForestGreen
 
#FF00FF	Fuchsia
 
#DCDCDC	Gainsboro
 
#F8F8FF	GhostWhite
 
#FFD700	Gold
 
#DAA520	GoldenRod
 
#008000	Green
 
#ADFF2F	GreenYellow
 
#808080	Grey
 
#F0FFF0	HoneyDew
 
#FF69B4	HotPink
 
#002395	ImperialBlue
 
#CD5C5C	IndianRed
 
#4B0082	Indigo
 
#FFFFF0	Ivory
 
#F0E68C	Khaki
 
#E6E6FA	Lavender
 
#FFF0F5	LavenderBlush
 
#7CFC00	LawnGreen
 
#FFFACD	LemonChiffon
 
#1A1110	Licorice
 
#ADD8E6	LightBlue
 
#F08080	LightCoral
 
#E0FFFF	LightCyan
 
#FAFAD2	LightGoldenRodYellow
 
#90EE90	LightGreen
 
#D3D3D3	LightGrey
 
#FFB6C1	LightPink
 
#FFA07A	LightSalmon
 
#20B2AA	LightSeaGreen
 
#87CEFA	LightSkyBlue
 
#8470FF	LightSlateBlue
 
#778899	LightSlateGrey
 
#B0C4DE	LightSteelBlue
 
#FFFFE0	LightYellow
 
#00FF00	Lime
 
#32CD32	LimeGreen
 
#FAF0E6	Linen
 
#800000	Maroon
 
#66CDAA	MediumAquaMarine
 
#0000CD	MediumBlue
 
#BA55D3	MediumOrchid
 
#9370DB	MediumPurple
 
#3CB371	MediumSeaGreen
 
#7B68EE	MediumSlateBlue
 
#00FA9A	MediumSpringGreen
 
#48D1CC	MediumTurquoise
 
#C71585	MediumVioletRed
 
#E3A869	Melon
 
#191970	MidnightBlue
 
#F5FFFA	MintCream
 
#FFE4E1	MistyRose
 
#FFE4B5	Moccasin
 
#FFDEAD	NavajoWhite
 
#000080	Navy
 
#FDF5E6	OldLace
 
#808000	Olive
 
#6B8E23	OliveDrab
 
#FFA500	Orange
 
#FF4500	OrangeRed
 
#DA70D6	Orchid
 
#002147	OxfordBlue
 
#EEE8AA	PaleGoldenRod
 
#98FB98	PaleGreen
 
#AFEEEE	PaleTurquoise
 
#DB7093	PaleVioletRed
 
#FFEFD5	PapayaWhip
 
#FFDAB9	PeachPuff
 
#33A1C9	Peacock
 
#32127A	PersianIndigo
 
#F77FBE	PersianPink
 
#CD853F	Peru
 
#FFC0CB	Pink
 
#DDA0DD	Plum
 
#B0E0E6	PowderBlue
 
#003153	PrussianBlue
 
#800080	Purple
 
#C76114	RawSienna
 
#FF0000	Red
 
#860111	RedDevil
 
#004040	RichBlack
 
#BC8F8F	RosyBrown
 
#4169E1	RoyalBlue
 
#9B111E	RubyRed
 
#8B4513	SaddleBrown
 
#FA8072	Salmon
 
#F4A460	SandyBrown
 
#92000A	Sangria
 
#308014	SapGreen
 
#2E8B57	SeaGreen
 
#321414	SealBrown
 
#FFF5EE	SeaShell
 
#A0522D	Sienna
 
#C0C0C0	Silver
 
#87CEEB	SkyBlue
 
#6A5ACD	SlateBlue
 
#708090	SlateGrey
 
#100C08	SmokeyBlack
 
#FFFAFA	Snow
 
#00FF7F	SpringGreen
 
#4682B4	SteelBlue
 
#CC3366	SteelPink
 
#D2B48C	Tan
 
#008080	Teal
 
#D8BFD8	Thistle
 
#FF6347	Tomato
 
#40E0D0	Turquoise
 
#66023C	TyrianPurple
 
#EE82EE	Violet
 
#D02090	VioletRed
 
#F5DEB3	Wheat
 
#FFFFFF	White
 
#F5F5F5	WhiteSmoke
 
#FFFF00	Yellow
 
#9ACD32	YellowGreen
 
#0014A8	Zaffre
Data/ingredients.txt
Show inline comments
 
new file 100644
 
// Desert Paint Lab Ingredients
 
// Name | Practical Paint Name | RGB values
 
// These should be kept in the order they show up on the paint bench
 
// NOTE: EarthLight was replaced with Falcon's Bait in T9.
 

	
 
Cabbage Juice   | Cabbage     | 128, 64, 144
 
Carrot          | Carrot      | 224, 112, 32
 
Clay            | Clay        | 128, 96, 32
 
Dead Tongue     | DeadTongue  | 112, 64, 64
 
Toad Skin       | ToadSkin    | 48, 96, 48
 
Falcons Bait    | FalconsBait | 128, 240, 224
 
Red Sand        | RedSand     | 144, 16, 24
 
Lead            | Lead        | 80, 80, 96
 
Silver Powder   | Silver	  | 16, 16, 32
 
Iron            | Iron		  | 96, 48, 32
 
Copper          | Copper	  | 64, 192, 192
 
Sulfur          | Sulfur	  | catalyst
 
Potash          | Potash	  | catalyst
 
Lime            | Lime		  | catalyst
 
Saltpeter       | Saltpeter	  | catalyst
Data/template/clips.txt
Show inline comments
 
new file 100644
 

...
 
\ No newline at end of file
Data/template/dp_reactions.txt
Show inline comments
 
new file 100644
 

...
 
\ No newline at end of file
Data/template/ingredients.txt
Show inline comments
 
new file 100644
 
// Ingredients are in the form:
 
// Name | RGB values | cost | enabled (Y/N) | bulk/normal | max items per paint (1-20)
 
//
 
// It is recommended to only change the cost value
 
// It is not recommended to set many of the ingredients above 10 per paint
 

	
 
Cabbage		| 128, 64, 144   | 8      | Y | bulk    | 10
 
Carrot		| 224, 112, 32   | 8      | Y | bulk    | 10
 
Clay		| 128, 96, 32    | 10     | Y | bulk    | 20
 
DeadTongue	| 112, 64, 64    | 500    | Y | normal  | 4
 
ToadSkin	| 48, 96, 48     | 500    | Y | normal  | 4
 
FalconsBait	| 128, 240, 224  | 10000  | Y | normal  | 4
 
RedSand		| 144, 16, 24    | 4      | Y | bulk    | 20
 
Lead		| 80, 80, 96     | 50     | Y | normal  | 6
 
Silver		| 16, 16, 32     | 50     | Y | normal  | 6
 
Iron		| 96, 48, 32     | 30     | Y | normal  | 8
 
Copper		| 64, 192, 192   | 30     | Y | normal  | 8
 
Sulfur      | catalyst       | 15     | Y | normal  | 1
 
Potash      | catalyst       | 50     | Y | normal  | 1
 
Lime        | catalyst       | 20     | Y | normal  | 1
 
Saltpeter   | catalyst       | 10     | Y | normal  | 1
DesertPaintCodex.csproj
Show inline comments
 
new file 100644
 
<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
 
    <OutputType>WinExe</OutputType>
 
    <TargetFramework>net5.0</TargetFramework>
 
    <Nullable>enable</Nullable>
 
  </PropertyGroup>
 
  <ItemGroup>
 
    <Folder Include="Models\" />
 
    <AvaloniaResource Include="Assets\**" />
 
    <None Remove="CodexTheme.xaml" />
 
  </ItemGroup>
 
  <ItemGroup>
 
    <PackageReference Include="Avalonia" Version="0.10.999-cibuild0014036-beta" />
 
    <PackageReference Include="Avalonia.Desktop" Version="0.10.999-cibuild0014036-beta" />
 
    <PackageReference Include="Avalonia.Diagnostics" Version="0.10.999-cibuild0014036-beta" />
 
    <PackageReference Include="Avalonia.ReactiveUI" Version="0.10.999-cibuild0014036-beta" />
 
    <PackageReference Include="System.Reactive" Version="5.0.0" />
 
  </ItemGroup>
 
  <ItemGroup>
 
    <None Update="Data\ingredients.txt">
 
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 
    </None>
 
    <None Update="Data\colors.txt">
 
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 
    </None>
 
    <None Update="Data\template\clips.txt">
 
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 
    </None>
 
    <None Update="Data\template\dp_reactions.txt">
 
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 
    </None>
 
    <None Update="Data\template\ingredients.txt">
 
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 
    </None>
 
  </ItemGroup>
 
</Project>
LICENSE.md
Show inline comments
 
new file 100644
 
The MIT License (MIT)
 
=====================
 

	
 
Copyright © `2021` `Tess Snider, Jason Maltzen`
 

	
 
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.
Models/ClipType.cs
Show inline comments
 
new file 100644
 

 
using System;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    [Flags]
 
    public enum ClipType
 
    {
 
        None      = 0,
 
        RedLow    = 0x01,
 
        GreenLow  = 0x02,
 
        BlueLow   = 0x04,
 
        RedHigh   = 0x08,
 
        GreenHigh = 0x10,
 
        BlueHigh  = 0x20
 
    }
 
}
Models/GeneratorRecipe.cs
Show inline comments
 
new file 100644
 
using System.ComponentModel;
 
using System.Runtime.CompilerServices;
 
using System.Text;
 
using JetBrains.Annotations;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class GeneratorRecipe : INotifyPropertyChanged
 
    {
 
        private PaintColor _color;
 
        public PaintColor Color { get => _color; set { _color = value; NotifyPropertyChanged(nameof(Color)); } }
 

	
 
        private string _recipe = "";
 
        public string Recipe { get => _recipe; set { _recipe = value; NotifyPropertyChanged(nameof(Recipe)); } }
 

	
 

	
 
        public GeneratorRecipe(PaintColor color)
 
        {
 
            _color = color;
 
        }
 

	
 
        public void DraftRecipe(PaintRecipe recipe)
 
        {
 
            StringBuilder sb = new();
 
            for (int i = 0; i < recipe.Reagents.Count; i++)
 
            {
 
                sb.Append(recipe.Reagents[i].Quantity);
 
                sb.Append(' ');
 
                sb.Append(recipe.Reagents[i].Name);
 
                if (i != recipe.Reagents.Count - 1)
 
                {
 
                    sb.Append(", ");
 
                }
 
            }
 

	
 
            Recipe = sb.ToString();
 
        }
 

	
 
        public void ClearRecipe()
 
        {
 
            Recipe = "";
 
        }
 
        
 
        
 
        public event PropertyChangedEventHandler? PropertyChanged;
 

	
 
        [NotifyPropertyChangedInvocator]
 
        private void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)
 
        {
 
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Models/InterfaceSize.cs
Show inline comments
 
new file 100644
 

 
namespace DesertPaintCodex.Models
 
{
 
    public enum InterfaceSize
 
    {
 
        Tiny = 0,
 
        Small = 1,
 
        Medium = 2,
 
        Large = 3,
 
        Huge = 4,
 
        Count
 
    }
 
}
Models/PaintColor.cs
Show inline comments
 
new file 100644
 
using System;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class PaintColor
 
    {
 
        private const byte  LightTextValueCutoff = 150;
 
        private const float RedIntensity   = 0.299f;
 
        private const float GreenIntensity = 0.587f;
 
        private const float BlueIntensity  = 0.114f;
 

	
 
        public byte Red { get; set; }
 
        public byte Blue { get; set; }
 
        public byte Green { get; set; }
 
        public string Name { get; private set; }
 

	
 
        public bool UseWhiteText => Red * RedIntensity + Green * GreenIntensity + Blue * BlueIntensity < LightTextValueCutoff;
 
        
 
        public PaintColor()
 
        {
 
            Name  = "Undefined";
 
            Red   = 0;
 
            Green = 0;
 
            Blue  = 0;
 
        }
 

	
 
        public PaintColor(string name, string hexRed, string hexGreen, string hexBlue)
 
        {
 
            Name = name;
 
            Red =   byte.Parse(hexRed,   System.Globalization.NumberStyles.AllowHexSpecifier);
 
            Green = byte.Parse(hexGreen, System.Globalization.NumberStyles.AllowHexSpecifier);
 
            Blue =  byte.Parse(hexBlue,  System.Globalization.NumberStyles.AllowHexSpecifier);
 
        }
 

	
 
        public PaintColor(byte red, byte green, byte blue)
 
        {
 
            Name  = "Undefined";
 
            Red   = red;
 
            Green = green;
 
            Blue  = blue;
 
        }
 

	
 
        public PaintColor(PaintColor other)
 
        {
 
            Name  = other.Name;
 
            Red   = other.Red;
 
            Green = other.Green;
 
            Blue  = other.Blue;
 
        }
 

	
 
        public int GetDistanceSquared(PaintColor otherColor)
 
        {
 
            return (int)(Math.Pow(Red - otherColor.Red, 2) +
 
                Math.Pow(Green - otherColor.Green, 2) +
 
                Math.Pow(Blue - otherColor.Blue, 2));
 
        }
 

	
 
        public void Clear()
 
        {
 
            Red   = 0;
 
            Green = 0;
 
            Blue  = 0;
 
        }
 

	
 
        public void Set(PaintColor other)
 
        {
 
            Red   = other.Red;
 
            Green = other.Green;
 
            Blue  = other.Blue;
 
            Name  = other.Name;
 
        }
 

	
 
        public string ToHexString()
 
        {
 
            return "#" + Red.ToString("X2") + Green.ToString("X2") + Blue.ToString("X2");
 
        }
 

	
 
        public override string ToString()
 
        {
 
            return "[" + Name + ", " + Red + ", " + Green + ", " + Blue + "]";
 
        }
 
    }
 
}
Models/PaintRecipe.cs
Show inline comments
 
new file 100644
 
using DesertPaintCodex.Services;
 
using System;
 
using System.Collections.Generic;
 
using System.Diagnostics;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class PaintRecipe
 
    {
 
        public const uint PaintRecipeMinConcentration  = 10;
 
        public const uint RibbonRecipeMinConcentration = 50;
 

	
 
        private struct RGB
 
        {
 
            public int R;
 
            public int G;
 
            public int B;
 
        };
 

	
 
        public class ReagentQuantity
 
        {
 
            public readonly string Name;
 
            public uint Quantity;
 

	
 
            public ReagentQuantity(string name, uint quantity)
 
            {
 
                Name     = name;
 
                Quantity = quantity;
 
            }
 

	
 
            public ReagentQuantity(ReagentQuantity other)
 
            {
 
                Name     = other.Name;
 
                Quantity = other.Quantity;
 
            }
 

	
 
            public override string ToString()
 
            {
 
                return $"{Name} {Quantity}";
 
            }
 
        };
 
        
 
        
 
        private readonly List<ReagentQuantity> _recipe   = new();
 
        private readonly List<string>          _reagents = new();
 

	
 
        private readonly PaintColor _reactedColor = new();
 
        private readonly PaintColor _baseColor    = new();
 

	
 
        private bool _recalculate;
 

	
 
        public PaintRecipe()
 
        {
 
        }
 

	
 
        public PaintRecipe(PaintRecipe other)
 
        {
 
            _recalculate = true;
 
            foreach (string reagentName in other._reagents)
 
            {
 
                _reagents.Add(reagentName);
 
            }
 
            foreach (ReagentQuantity copyReagent in other._recipe)
 
            {
 
                ReagentQuantity ingredient = new(copyReagent);
 
                _recipe.Add(ingredient);
 
            }
 
        }
 

	
 
        public void CopyFrom(PaintRecipe other)
 
        {
 
            _recalculate = true;
 
            _reagents.Clear();
 
            foreach (string reagentName in other._reagents)
 
            {
 
                _reagents.Add(reagentName);
 
            }
 
            _recipe.Clear();
 
            foreach (ReagentQuantity otherIngredient in other._recipe)
 
            {
 
                ReagentQuantity ingredient = new(otherIngredient);
 
                _recipe.Add(ingredient);
 
            }
 
        }
 

	
 
        public override string ToString()
 
        {
 
            string result = PaletteService.FindNearest(ReactedColor);
 
            result += " |";
 
            foreach (ReagentQuantity ri in _recipe)
 
            {
 
                result += " " + ri;
 
            }
 
            return result;
 
        }
 

	
 
        public List<ReagentQuantity> Reagents => _recipe;
 

	
 
        public PaintColor ReactedColor
 
        {
 
            get
 
            {
 
                if (!_recalculate) return _reactedColor;
 
                ComputeBaseColor();
 
                ComputeReactedColor();
 
                _recalculate = false;
 
                return _reactedColor;
 
            }
 
        }
 

	
 
        public PaintColor BaseColor
 
        {
 
            get
 
            {
 
                if (!_recalculate) return _baseColor;
 
                ComputeBaseColor();
 
                ComputeReactedColor();
 
                _recalculate = false;
 
                return _baseColor;
 
            }
 
        }
 

	
 
        public void AddReagent(string reagentName, uint quantity = 1)
 
        {
 
            if (quantity == 0)
 
            {
 
                return;
 
            }
 
            Reagent reagent = ReagentService.GetReagent(reagentName);
 

	
 
            ReagentQuantity? reagentQty = _recipe.Find(x => x.Name.Equals(reagentName));
 
            if (reagentQty != null)
 
            {
 
                if (!reagent.IsCatalyst)
 
                {
 
                    reagentQty.Quantity += quantity;
 
                }
 
            }
 
            else
 
            {
 
                ReagentQuantity newReagentQty = new(reagentName, reagent.IsCatalyst ? 1 : quantity);
 
                _recipe.Add(newReagentQty);
 
            }
 
            _reagents.Add(reagentName);
 
            _recalculate = true;
 
        }
 

	
 
        public void Clear()
 
        {
 
            _reagents.Clear();
 
            _recipe.Clear();
 
            _recalculate = true;
 
        }
 

	
 
        private static byte CalculateColor(int baseSum, uint pigmentCount, int reactSum)
 
        {
 
            // Changed to Math.Floor from Math.Round, since Round appears to be incorrect.
 
            return (byte)Math.Max(Math.Min(Math.Floor((((float)baseSum / pigmentCount) + reactSum)), 255), 0);
 
        }
 

	
 
        // Compute the color including reactions based on the player's profile
 
        private void ComputeReactedColor()
 
        {
 
            RGB baseRGB;
 
            baseRGB.R = 0;
 
            baseRGB.G = 0;
 
            baseRGB.B = 0;
 
            RGB reactionColor;
 
            reactionColor.R = 0;
 
            reactionColor.G = 0;
 
            reactionColor.B = 0;
 

	
 
            uint pigmentCount = 0;
 
            
 
            ReactionSet? reactions = ProfileManager.CurrentProfile?.Reactions;
 
            Debug.Assert(reactions != null);
 
            
 
            // track visited reagents so the reaction is only applied once
 
            HashSet<string> reagentSet   = new();
 
            List<Reagent>                  prevReagents = new();
 

	
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 

	
 
                if (!reagent.IsCatalyst)
 
                {
 
                    Debug.Assert(reagent.Color != null);
 
                    baseRGB.R += (reagent.Color.Red   * (int)ingredient.Quantity); 
 
                    baseRGB.G += (reagent.Color.Green * (int)ingredient.Quantity);
 
                    baseRGB.B += (reagent.Color.Blue  * (int)ingredient.Quantity);
 
                    pigmentCount += ingredient.Quantity;
 
                }
 

	
 
                if (!reagentSet.Contains(reagentName) && reagentSet.Count <= 4)
 
                {
 
                    reagentSet.Add(reagentName);
 
                    // Run reactions.
 
                    foreach (Reagent otherReagent in prevReagents)
 
                    {
 
                        Reaction? reaction = reactions.Find(otherReagent, reagent);
 
                        if (reaction != null)
 
                        {
 
                            reactionColor.R += reaction.Red;
 
                            reactionColor.G += reaction.Green;
 
                            reactionColor.B += reaction.Blue;
 
                        }
 
                    }
 
                    prevReagents.Add(reagent);
 
                }
 
            }
 
            _reactedColor.Red = CalculateColor(baseRGB.R, pigmentCount, reactionColor.R);
 
            _reactedColor.Green = CalculateColor(baseRGB.G, pigmentCount, reactionColor.G);
 
            _reactedColor.Blue = CalculateColor(baseRGB.B, pigmentCount, reactionColor.B);
 
        }
 

	
 
        // Compute the base color without any reactions
 
        private void ComputeBaseColor()
 
        {
 
            RGB baseRGB;
 
            baseRGB.R = 0;
 
            baseRGB.G = 0;
 
            baseRGB.B = 0;
 
            uint pigmentCount = 0;
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 
                
 
                if (reagent.IsCatalyst) continue;
 
                
 
                Debug.Assert(reagent.Color != null);
 
                baseRGB.R    += (reagent.Color.Red   * (int)ingredient.Quantity);
 
                baseRGB.G    += (reagent.Color.Green * (int)ingredient.Quantity);
 
                baseRGB.B    += (reagent.Color.Blue  * (int)ingredient.Quantity);
 
                pigmentCount += ingredient.Quantity;
 
            }
 
            _baseColor.Red   = CalculateColor(baseRGB.R, pigmentCount, 0);
 
            _baseColor.Green = CalculateColor(baseRGB.G, pigmentCount, 0);
 
            _baseColor.Blue  = CalculateColor(baseRGB.B, pigmentCount, 0);
 
        }
 

	
 
        // Compute the base color without any reactions
 
        public uint Cost
 
        {
 
            get
 
            {
 
                uint cost = 0;
 
                foreach (ReagentQuantity ingredient in _recipe)
 
                {
 
                    string reagentName = ingredient.Name;
 
                    if (string.IsNullOrEmpty(reagentName))
 
                    {
 
                        continue;
 
                    }
 

	
 
                    Reagent reagent = ReagentService.GetReagent(reagentName);
 
                    cost += (reagent.Cost * ingredient.Quantity);
 
                }
 
                return cost;
 
            }
 
        }
 

	
 
        public uint GetBulkCost(uint quantity)
 
        {
 
            uint cost = 0;
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 
                cost += (reagent.Cost * ingredient.Quantity);
 
            }
 
            uint batchCount = (uint)Math.Ceiling((double)quantity / cost);  // number of batches require to make quantity
 
            return batchCount * cost;
 
        }
 

	
 
        public bool IsValidForConcentration(uint concentration)
 
        {
 
            uint weight = 0;
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName))
 
                {
 
                    continue;
 
                }
 

	
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 
                if (!reagent.IsCatalyst)
 
                {
 
                    weight += ingredient.Quantity;
 
                }
 
            }
 
            return (weight >= concentration);
 
        }
 
        public bool HasMissingReactions()
 
        {
 
            ReactionSet? reactions = ProfileManager.CurrentProfile?.Reactions;
 
            Debug.Assert(reactions != null);
 

	
 
            HashSet<string> reagentSet = new();
 
            List<Reagent> prevReagents = new();
 
            
 
            foreach (ReagentQuantity ingredient in _recipe)
 
            {
 
                if (reagentSet.Count > 4) return false;
 
                
 
                string reagentName = ingredient.Name;
 
                if (string.IsNullOrEmpty(reagentName)) continue;
 
                if (reagentSet.Contains(reagentName)) continue;
 
                reagentSet.Add(reagentName);
 
                
 
                Reagent reagent = ReagentService.GetReagent(reagentName);
 

	
 
                // Find reactions.
 
                foreach (Reagent otherReagent in prevReagents)
 
                {
 
                    Reaction? reaction = reactions.Find(otherReagent, reagent);
 
                    if (reaction == null) return true;
 
                }
 
                prevReagents.Add(reagent);
 
            }
 

	
 
            return false;
 
        }
 
    }
 
}
Models/PlayerProfile.cs
Show inline comments
 
new file 100644
 
using System;
 
using System.IO;
 
using System.IO.Compression;
 
using System.Collections.Generic;
 
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: (?<colorname>(\w*\s)*\w+)\s*");
 
        private static readonly Regex _recipeIngredientRegex = new(@"(?<ingredient>(\w+\s)?\w+)\s*\|\s*(?<quantity>\d+)\s*");
 
        
 
        private Settings ProfileSettings { get; } = new();
 

	
 
        public string Directory { get; }
 

	
 
        public string Name { get; private set; }
 
        
 
        public ReactionSet Reactions { get; } = new();
 

	
 
        private Dictionary<string, Dictionary<string, ClipType>> Clippers { get; } = new();
 

	
 
        public string ReagentFile { get; }
 

	
 
        public Dictionary<string, PaintRecipe> Recipes { get; } = new();
 

	
 
        public Dictionary<string, PaintRecipe> 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);
 
            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 reagent1  = tokens[0].Trim();
 
                    string reagent2  = tokens[1].Trim();
 
                    string colorCode = tokens[2].Trim();
 
                    string change1   = tokens[3].Trim();
 
                    string change2   = tokens[4].Trim();
 
                    // Write reaction.
 
                    switch (colorCode)
 
                    {
 
                        case "W":
 
                            WriteReaction(writer, reagent1, reagent2, change1, change1, change1);
 
                            WriteReaction(writer, reagent2, reagent1, change2, change2, change2);
 
                            break;
 
                        case "R":
 
                            WriteReaction(writer, reagent1, reagent2, change1, "0", "0");
 
                            WriteReaction(writer, reagent2, reagent1, change2, "0", "0");
 
                            break;
 
                        case "G":
 
                            WriteReaction(writer, reagent1, reagent2, "0", change1, "0");
 
                            WriteReaction(writer, reagent2, reagent1, "0", change2, "0");
 
                            break;
 
                        case "B":
 
                            WriteReaction(writer, reagent1, reagent2, "0", "0", change1);
 
                            WriteReaction(writer, reagent2, reagent1, "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 importDir)
 
        {
 
            // Convert old file.
 
            ConvertFromPP(
 
                Path.Combine(importDir, "reactions.txt"),
 
                _reactFile);
 
            try
 
            {
 
                // If there is an ingredients file, move it in.
 
                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)
 
        {
 
            ZipFile.ExtractToDirectory(file, Directory);
 
        }
 

	
 
        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 StreamReader(_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<string, ClipType>());
 
                    }
 
                    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 StreamWriter(_clipFile, false))
 
            {
 
                foreach (var item1 in Clippers)
 
                {
 
                    foreach (var item2 in item1.Value)
 
                    {
 
                        writer.WriteLine(item1.Key + " " + item2.Key + " " + (int)item2.Value);
 
                    }
 
                }
 
            }
 
        }
 

	
 
        public ClipType PairClipStatus(string reagent1, string reagent2)
 
        {
 
            if (Clippers.TryGetValue(reagent1, out var item1))
 
            {
 
                if (item1.TryGetValue(reagent2, out var clipType))
 
                {
 
                    return clipType;
 
                }
 
            }
 
            return ClipType.None;
 
        }
 

	
 
        private void LoadRecipes(Dictionary<string, PaintRecipe> 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<string, PaintRecipe> recipeDict, string filename)
 
        {
 
            string recipeFile = Path.Combine(Directory, filename);
 
            
 
            using StreamWriter writer = new(recipeFile, false);
 
            
 
            foreach (KeyValuePair<string, PaintRecipe> 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<string, PaintRecipe> 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<string, PaintRecipe> 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 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));
 
            }
 
        }
 
    }
 
}
Models/Reaction.cs
Show inline comments
 
new file 100644
 
namespace DesertPaintCodex.Models
 
{
 
    public class Reaction
 
    {
 
        public int Red { get; }
 
        public int Green { get; }
 
        public int Blue { get; }
 
        public bool Exported { get; set; }
 

	
 
        public Reaction(int r, int g, int b)
 
        {
 
            Red = r;
 
            Green = g;
 
            Blue = b;
 
        }
 

	
 
        public override string ToString()
 
        {
 
            return "[" + Red + ", " + Green + ", " + Blue + "]";
 
        }
 
    }
 
}
Models/ReactionSet.cs
Show inline comments
 
new file 100644
 
using System.Collections.Generic;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class ReactionSet
 
    {
 
        // ingredient -> [ingredient, reaction]
 
        private readonly Dictionary<string, Dictionary<string, Reaction?>> _reactions = new();
 

	
 
        public Reaction? Find(Reagent reagent1, Reagent reagent2)
 
        {
 
            Reaction? reaction = null;
 
            _reactions.TryGetValue(reagent1.PracticalPaintName, out Dictionary<string, Reaction?>? secondReagentDict);
 
            secondReagentDict?.TryGetValue(reagent2.PracticalPaintName, out reaction);
 
            return reaction;
 
        }
 

	
 
        public void Set(Reagent reagent1, Reagent reagent2, Reaction? reaction)
 
        {
 
            _reactions.TryGetValue(reagent1.PracticalPaintName, out Dictionary<string, Reaction?>? secondReagentDict);
 
            if (secondReagentDict == null)
 
            {
 
                secondReagentDict = new Dictionary<string, Reaction?>();
 
                _reactions.Add(reagent1.PracticalPaintName, secondReagentDict);
 
            }
 
            secondReagentDict[reagent2.PracticalPaintName] = reaction;
 
        }
 

	
 
        public void Remove(Reagent reagent1, Reagent reagent2)
 
        {
 
            _reactions.TryGetValue(reagent1.PracticalPaintName, out Dictionary<string, Reaction?>? secondReagentDict);
 
            secondReagentDict?.Remove(reagent2.PracticalPaintName);
 
        }
 

	
 
        public void Clear()
 
        {
 
            _reactions.Clear();
 
        }
 
    }
 
}
Models/ReactionTest.cs
Show inline comments
 
new file 100644
 
using System;
 
using System.ComponentModel;
 
using System.Diagnostics;
 
using System.Runtime.CompilerServices;
 
using System.Threading.Tasks;
 
using DesertPaintCodex.Services;
 
using JetBrains.Annotations;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class ReactionTest : INotifyPropertyChanged, IProgress<float>, IComparable<ReactionTest>
 
    {
 
        public enum TestState
 
        {
 
            Unset = -1,
 
            Untested,
 
            Scanning,
 
            LabNotFound,
 
            ClippedResult,
 
            GoodResult,
 
            Saved
 
        }
 

	
 
        public Reagent Reagent1 { get; }
 
        public Reagent Reagent2 { get; }
 

	
 
        private Reagent? _bufferReagent;
 
        public Reagent? BufferReagent
 
        {
 
            get => _bufferReagent;
 
            set
 
            {
 
                if (_bufferReagent == value) return;
 
                _bufferReagent = value;
 
                _recipe.Clear();
 
                if (_bufferReagent == null)
 
                {
 
                    _recipe.AddReagent(Reagent1.Name);
 
                    _recipe.AddReagent(Reagent2.Name);
 
                }
 
                else
 
                {
 
                    _recipe.AddReagent(_bufferReagent.Name);
 
                    _recipe.AddReagent(Reagent1.Name);
 
                    _recipe.AddReagent(Reagent2.Name);
 
                }
 
                NotifyPropertyChanged(nameof(BufferReagent));
 
                NotifyPropertyChanged(nameof(HypotheticalColor));
 
                NotifyPropertyChanged(nameof(CanScan));
 
            }
 
        }
 

	
 
        private ClipType _clipType;
 
        public ClipType Clipped {
 
            get => _clipType;
 
            set
 
            {
 
                _clipType = value;
 
                NotifyPropertyChanged(nameof(Clipped));
 
            }
 
        }
 

	
 
        public bool IsAllCatalysts { get; }
 

	
 
        private Reaction? _reaction;
 
        public Reaction? Reaction { get => _reaction; set { _reaction = value; NotifyPropertyChanged(nameof(Reaction)); } }
 

	
 
        private Reaction? _badReaction;
 
        public Reaction? BadReaction { get => _badReaction; set { _badReaction = value; NotifyPropertyChanged(nameof(BadReaction)); } }
 

	
 
        private int _scanProgress;
 
        public int ScanProgress { get => _scanProgress; set { _scanProgress = value; NotifyPropertyChanged(nameof(ScanProgress)); } }
 
        
 
        
 
        private TestState _state;
 

	
 
        public TestState State
 
        {
 
            get => _state;
 
            set
 
            {
 
                _state = value;
 
                NotifyPropertyChanged(nameof(State));
 
                NotifyPropertyChanged(nameof(Requires3Way));
 
                NotifyPropertyChanged(nameof(CanScan));
 
                NotifyPropertyChanged(nameof(IsScanning));
 
                NotifyPropertyChanged(nameof(HasResults));
 
                NotifyPropertyChanged(nameof(HasReaction));
 
                NotifyPropertyChanged(nameof(CanClear));
 
                NotifyPropertyChanged(nameof(CanSave));
 
                NotifyPropertyChanged(nameof(NoLab));
 
            }
 
        }
 

	
 
        public bool Requires3Way =>
 
            (State == TestState.ClippedResult) || IsAllCatalysts;
 

	
 
        public bool CanScan => (State == TestState.Untested) || (State == TestState.LabNotFound) ||
 
            ((State == TestState.ClippedResult) && (BufferReagent != null));
 
        
 
        public bool IsScanning => State == TestState.Scanning;
 

	
 
        public bool HasResults => State is TestState.ClippedResult or TestState.GoodResult or TestState.LabNotFound;
 

	
 
        public bool HasReaction => State is TestState.ClippedResult or TestState.GoodResult or TestState.Saved;
 
        
 
        public bool CanClear => State is TestState.ClippedResult or TestState.GoodResult or TestState.Saved;
 
        
 
        public bool CanSave => State == TestState.GoodResult;
 

	
 
        public bool NoLab => State == TestState.LabNotFound;
 
        
 

	
 
        public PaintColor? HypotheticalColor => (IsAllCatalysts && (BufferReagent == null)) ? null : _recipe.BaseColor;
 

	
 
        private PaintColor? _observedColor;
 
        public PaintColor? ObservedColor { get => _observedColor; set { _observedColor = value; NotifyPropertyChanged(nameof(ObservedColor)); } }
 

	
 
        private readonly PaintRecipe _recipe = new();
 

	
 
        public bool IsStub { get; }
 

	
 

	
 
        public ReactionTest(Reagent reagent1, Reagent reagent2, Reaction? reaction, ClipType clipType, bool isStub = false)
 
        {
 
            Reagent1 = reagent1;
 
            Reagent2 = reagent2;
 
            IsAllCatalysts = reagent1.IsCatalyst && reagent2.IsCatalyst;
 
            Clipped = clipType;
 
            Reaction = reaction;
 
            State = (reaction != null) ? TestState.Saved :
 
                (clipType == ClipType.None) ? TestState.Untested : TestState.ClippedResult;
 
            _recipe.AddReagent(reagent1.Name);
 
            _recipe.AddReagent(reagent2.Name);
 
            IsStub = isStub;
 
        }
 

	
 
        
 
        #region Actions
 
        public async Task StartScan()
 
        {
 
            Clipped = ClipType.None;
 
            ScanProgress = 0;
 
            Reaction = null;
 
            BadReaction = null;
 
            State = TestState.Scanning;
 
            bool foundLab = await ReactionScannerService.Instance.CaptureReactionAsync(this);
 
            if (foundLab)
 
            {
 
                ObservedColor = ReactionScannerService.Instance.RecordedColor;
 
                if (_observedColor != null)
 
                {
 
                    Clipped = _observedColor.Red switch
 
                        {
 
                            0   => ClipType.RedLow,
 
                            255 => ClipType.RedHigh,
 
                            _   => ClipType.None
 
                        }
 
                        | _observedColor.Green switch
 
                        {
 
                            0   => ClipType.GreenLow,
 
                            255 => ClipType.GreenHigh,
 
                            _   => ClipType.None
 
                        }
 
                        | _observedColor.Blue switch
 
                        {
 
                            0   => ClipType.BlueLow,
 
                            255 => ClipType.BlueHigh,
 
                            _   => ClipType.None
 
                        };
 

	
 
                    if (Clipped == ClipType.None)
 
                    {
 
                        State = TestState.GoodResult;
 
                        Reaction = CalculateReaction();
 

	
 
                    }
 
                    else
 
                    {
 
                        State = TestState.ClippedResult;
 
                        BadReaction = CalculateReaction();
 
                    }
 
                }
 
            }
 
            else
 
            {
 
                Debug.WriteLine("ERROR: Lab UI not found.");
 
                State = TestState.LabNotFound;
 
            }
 
        }
 

	
 
        public void CancelScan()
 
        {
 
            ReactionScannerService.Instance.CancelScan();
 
            State = TestState.Untested;
 
        }
 

	
 
        public void MarkInert()
 
        {
 
            Reaction = new Reaction(0, 0, 0);
 
            State = TestState.GoodResult;
 
        }
 

	
 
        public void ClearReaction()
 
        {
 
            PlayerProfile? profile = ProfileManager.CurrentProfile;
 
            if (profile == null) return;
 
            profile.Reactions.Remove(Reagent1, Reagent2);
 
            if (State == TestState.Saved)
 
            {
 
                profile.Save();
 
            }
 
            
 
            Reaction = null;
 
            BadReaction = null;
 
            State = TestState.Untested;
 
        }
 

	
 
        public void SaveReaction()
 
        {
 
            PlayerProfile? profile = ProfileManager.CurrentProfile;
 
            if (profile == null) return;
 
            profile.Reactions.Set(Reagent1, Reagent2, Reaction);
 
            profile.Save();
 
            State = TestState.Saved;
 
        }
 
        
 
        #endregion
 
        
 
        #region Internals
 

	
 
        private Reaction? CalculateReaction()
 
        {
 
            if (ProfileManager.CurrentProfile == null) return null;
 
            if (HypotheticalColor == null) return null;
 
            if (ObservedColor == null) return null;
 

	
 
            if (Requires3Way)
 
            {
 
                if (BufferReagent == null) return null;
 
                return ReactionScannerService.Calculate3WayReaction(ProfileManager.CurrentProfile, HypotheticalColor,
 
                    ObservedColor, BufferReagent, Reagent1, Reagent2);
 
            }
 
            return ReactionScannerService.CalculateReaction(HypotheticalColor, ObservedColor);
 
        }
 
        
 
        
 
        #endregion
 
        
 
        
 

	
 
        #region Interface Implementations
 
        
 
        public event PropertyChangedEventHandler? PropertyChanged;
 

	
 
        [NotifyPropertyChangedInvocator]
 
        private void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)
 
        {
 
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
 
        }
 

	
 
        public void Report(float value)
 
        {
 
            ScanProgress = (int)Math.Round(value * 100);
 
        }
 
        
 
        #endregion
 

	
 
        public int CompareTo(ReactionTest? other)
 
        {
 
            if (other == null) return 1;
 
            
 
            if (Clipped == ClipType.None)
 
            {
 
                if (other.Clipped != ClipType.None) return -1;
 
            }
 
            else if (other.Clipped == ClipType.None) return 1;
 

	
 
            if (IsAllCatalysts)
 
            {
 
                if (!other.IsAllCatalysts) return 1;
 
            }
 
            else if (other.IsAllCatalysts) return -1;
 
            
 
            return string.CompareOrdinal(Reagent1.Name, other.Reagent1.Name) switch
 
            {
 
                < 0 => -1,
 
                > 0 => 1,
 
                _   => string.CompareOrdinal(Reagent2.Name, other.Reagent2.Name)
 
            };
 
        }
 
    }
 
}
Models/ReactionTestService.cs
Show inline comments
 
new file 100644
 
using System.Collections.Generic;
 
using System.Collections.ObjectModel;
 
using System.Diagnostics;
 
using System.Linq;
 
using DesertPaintCodex.Services;
 
using DynamicData;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public static class ReactionTestService
 
    {
 
        private static readonly List<ReactionTest> _allTests = new();
 
        
 
        static ReactionTestService()
 
        {
 
            
 
        }
 

	
 
        public static void Initialize()
 
        {
 
            _allTests.Clear();
 
            PlayerProfile? profile = ProfileManager.CurrentProfile;
 
            
 
            Debug.Assert(profile != null);
 
            
 
            List<string> reagentNames = ReagentService.Names;
 

	
 
            foreach (Reagent reagent1 in reagentNames.Select(ReagentService.GetReagent))
 
            {
 
                foreach (Reagent reagent2 in reagentNames.Select(ReagentService.GetReagent))
 
                {
 
                    if (reagent1 == reagent2) continue;
 

	
 
                    Reaction? reaction = profile.FindReaction(reagent1, reagent2);
 
                    ClipType clipType = profile.PairClipStatus(reagent1.Name, reagent2.Name);
 
                    
 
                    ReactionTest test = new(reagent1, reagent2, reaction, clipType)
 
                    {
 
                        Clipped  = clipType,
 
                        Reaction = reaction,
 
                       
 
                    };
 

	
 
                    _allTests.Add(test);
 
                }
 
            }
 
        }
 

	
 
        public static void PopulateRemainingTests(ObservableCollection<ReactionTest> collection)
 
        {
 
            collection.Clear();
 
            collection.AddRange(_allTests.Where(test => test.State != ReactionTest.TestState.Saved).OrderBy(test => test));
 
        }
 
        
 
        public static void PopulateCompletedTests(ObservableCollection<ReactionTest> collection)
 
        {
 
            collection.Clear();
 
            collection.AddRange(_allTests.Where(test => test.State == ReactionTest.TestState.Saved).OrderBy(test => test));
 
        }
 
        
 
    }
 
}
...
 
\ No newline at end of file
Models/Reagent.cs
Show inline comments
 
new file 100644
 
using System;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class Reagent
 
    {
 
        private uint _cost;
 
        private uint _recipeMax = 10;
 

	
 
        public bool IsCatalyst { get; }
 
        public PaintColor? Color { get; }
 
        public string Name { get; }
 
        public string PracticalPaintName { get; }
 
        public bool Enabled { get; set; }
 

	
 
        public uint Cost
 
        {
 
            get => _cost;
 
            set => _cost = Math.Max(1, value);
 
        }
 

	
 
        public uint RecipeMax
 
        {
 
            get => _recipeMax;
 
            set
 
            {
 
                if (!IsCatalyst)
 
                {
 
                    _recipeMax = Math.Max(0, value);
 
                }
 
            }
 
        }
 

	
 
        // catalyst
 
        public Reagent(string name, string ppName)
 
        {
 
            Name = name;
 
            PracticalPaintName = ppName;
 
            Cost = 2;
 
            Enabled = true;
 
            RecipeMax = 1;
 
            IsCatalyst = true;
 
        }
 

	
 
        public Reagent(string name, string ppName, byte red, byte green, byte blue)
 
        {
 
            Color = new PaintColor(red, green, blue);
 
            Name = name;
 
            PracticalPaintName = ppName;
 
            Cost = 1;
 
            RecipeMax = 10;
 
            Enabled = true;
 
            IsCatalyst = false;
 
        }
 

	
 
        public override string ToString()
 
        {
 
            if (IsCatalyst)
 
            {
 
                return "[" + Name + ", catalyst]";
 
            }
 
            else
 
            {
 
                return "[" + Name + ", " + Color + "]";
 
            }
 
        }
 
    }
 
}
Models/RecipeItem.cs
Show inline comments
 
new file 100644
 
using System.ComponentModel;
 
using System.Runtime.CompilerServices;
 
using JetBrains.Annotations;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class RecipeItem : INotifyPropertyChanged
 
    {
 
        private const uint PigmentMax = 25;
 
        private Reagent _reagent;
 

	
 
        public Reagent Reagent
 
        {
 
            get => _reagent;
 
            set
 
            {
 
                _reagent = value;
 
                NotifyPropertyChanged(nameof(Reagent));
 
            }
 
        }
 

	
 
        private uint _quantity;
 

	
 
        public uint Quantity
 
        {
 
            get => _quantity;
 
            set
 
            {
 
                _quantity = value;
 
                NotifyPropertyChanged(nameof(Quantity));
 
            }
 
        }
 

	
 
        private uint _maxQty;
 

	
 
        public uint MaxQty
 
        {
 
            get => _maxQty;
 
            set
 
            {
 
                _maxQty = value;
 
                NotifyPropertyChanged(nameof(MaxQty));
 
            }
 
        }
 

	
 
        private bool _first;
 

	
 
        public bool First
 
        {
 
            get => _first;
 
            set
 
            {
 
                _first = value;
 
                NotifyPropertyChanged(nameof(First));
 
            }
 
        }
 

	
 
        private bool _last;
 

	
 
        public bool Last
 
        {
 
            get => _last;
 
            set
 
            {
 
                _last = value;
 
                NotifyPropertyChanged(nameof(Last));
 
            }
 
        }
 

	
 
        private bool _unused;
 
        public bool Unused
 
        {
 
            get => _unused;
 
            set
 
            {
 
                _unused = value;
 
                NotifyPropertyChanged(nameof(Unused));
 
            }
 
        }
 

	
 
        public RecipeItem(Reagent reagent, uint quantity)
 
        {
 
            _reagent = reagent;
 
            Quantity = quantity;
 
            MaxQty = reagent.IsCatalyst ? 1 : PigmentMax;
 
        }
 

	
 
        public override string ToString()
 
        {
 
            return $"{Reagent.Name} {Quantity}";
 
        }
 

	
 
        public event PropertyChangedEventHandler? PropertyChanged;
 

	
 
        [NotifyPropertyChangedInvocator]
 
        private void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)
 
        {
 
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Models/RecipeSearchNode.cs
Show inline comments
 
new file 100644
 
using System;
 
using System.Collections.Generic;
 
using System.IO;
 
using System.Text.RegularExpressions;
 

	
 
namespace DesertPaintCodex.Models
 
{
 
    public class RecipeSearchNode
 
    {
 
        private readonly uint[] _reagents;
 
        private readonly uint _invalidReagent;
 

	
 
        private int _nextReagentPos;
 
        public int ReagentCount => _nextReagentPos;
 

	
 
        public uint MinConcentration { get; set; } = 10;
 

	
 
        private readonly bool[] _reagentInUse;
 
        private readonly List<Reagent> _costSortedReagents;
 
        public PaintRecipe? TestRecipe { get; set; }
 

	
 
        public uint CurrentTargetQuantity { get; set; }
 
        public uint MaxConcentration { get; set; }
 
        public uint UsedQuantity { get; private set; }
 
        public uint CatalystCount { get; set; }
 
        public uint FullQuantityDepth { get; set; }
 
        public uint FullQuantity { get; set; }
 
        public uint MinReagents { get; set; }
 

	
 
        private uint _maxReagents;
 
        public uint MaxReagents
 
        {
 
            get => _maxReagents;
 
            set
 
            {
 
                _maxReagents = value;
 
                CurrentWeights = new uint[_maxReagents];
 
            }
 
        }
 

	
 
        public uint[] CurrentWeights { get; private set; }
 
        
 
        public uint LastReagent => _reagents[_nextReagentPos - 1];
 

	
 
        public RecipeSearchNode(RecipeSearchNode other)
 
        {
 
            _costSortedReagents = new List<Reagent>(other._costSortedReagents);
 
            _reagents = new uint[_costSortedReagents.Count];
 
            _invalidReagent = (uint)_costSortedReagents.Count;
 
            for (int i = 0; i < _costSortedReagents.Count; ++i)
 
            {
 
                this._reagents[i] = other._reagents[i];
 
            }
 
            _reagentInUse = new bool[_costSortedReagents.Count];
 
            for (uint i = 0; i < _costSortedReagents.Count; ++i)
 
            {
 
                _reagentInUse[i] = other._reagentInUse[i];
 
            }
 
            _nextReagentPos = other.ReagentCount;
 

	
 
            CurrentTargetQuantity = other.CurrentTargetQuantity;
 
            MaxConcentration = other.MaxConcentration;
 
            UsedQuantity = other.UsedQuantity;
 
            CatalystCount = other.CatalystCount;
 
            FullQuantityDepth = other.FullQuantityDepth;
 
            FullQuantity = other.FullQuantity;
 
            MinReagents = other.MinReagents;
 
            MaxReagents = other.MaxReagents;
 
            CurrentWeights = new uint[MaxReagents];
 
            for (int i = 0; i < MaxReagents; ++i)
 
            {
 
                CurrentWeights[i] = other.CurrentWeights[i];
 
            }
 
        }
 

	
 
        public RecipeSearchNode(List<Reagent> costSortedReagents, uint[] reagents)
 
        {
 
            _costSortedReagents = new List<Reagent>(costSortedReagents);
 
            _reagents = new uint[costSortedReagents.Count];
 
            _invalidReagent = (uint)costSortedReagents.Count;
 
            _nextReagentPos = reagents.Length;
 
            for (int i = _reagents.Length - 1; i >= _reagents.Length; --i)
 
            {
 
                reagents[i] = _invalidReagent;
 
            }
 
            for (int i = reagents.Length - 1; i >= 0; --i)