Changeset - 7117d2e703c8
[Not reviewed]
default
1 12 0
Tess Snider (Malkyne) - 3 years ago 2021-07-19 09:49:18
this@malkyne.org
Updated scanner for Tale 10, and fixed several bugs.
13 files changed with 181 insertions and 217 deletions:
0 comments (0 inline, 0 general)
Models/InterfaceSize.cs
Show inline comments
 
deleted file
Services/ReactionScannerService.cs
Show inline comments
 
using System;
 
using System.Diagnostics;
 
using System.IO;
 
using System.Drawing;
 
using System.Drawing.Imaging;
 
using System.Threading;
 
using System.Threading.Tasks;
 
using DesertPaintCodex.Models;
 
using DesertPaintCodex.Util;
 

	
 
namespace DesertPaintCodex.Services
 
{
 
    public class ReactionScannerService
 
    {
 
        private enum LogVerbosity
 
        {
 
            Low,
 
            Normal,
 
            High,
 
            Excessive
 
        }
 
        
 
        // Swatch is 301x20 solid color, 1px darker left/top, 1px black outer left/top, 1px light right/bottom, 1px bright right/bottom
 
        // top-right and bottom-left are bright instead of black
 

	
 
        // Color bars are 4x302 solid color, 2px border on all sides (darker left/top, lighter bottom/right
 

	
 
        private static readonly int[] SWATCH_HEIGHT = 
 
        {
 
            24, // tiny
 
            24, // small
 
            24, // medium
 
            24, // large
 
            24 // huge
 
        };
 

	
 
        private static readonly int[] SWATCH_WIDTH =
 
        {
 
            306, // tiny
 
            306, // small
 
            306, // medium
 
            320, // large
 
            350 // huge
 
        };
 

	
 
        private static readonly int[] COLOR_BAR_WIDTH =
 
        {
 
            306, // tiny
 
            306, // small -- includes left and right borders
 
            306, // medium
 
            320, // large
 
            350 // huge
 
        };
 

	
 
        private static readonly int[] RED_BAR_SPACING =
 
        {
 
            32, // tiny
 
            32, // small
 
            32, // medium
 
            32, // large
 
            32 // huge
 
        };
 

	
 
        private static readonly int[] GREEN_BAR_SPACING =
 
        {
 
            42, // tiny
 
            42, // small
 
            42, // medium
 
            42, // large
 
            42 // huge
 
        };
 
        private const int SwatchHeight    = 24;
 
        private const int RedBarSpacing   = 32;
 
        private const int GreenBarSpacing = 42;
 
        private const int BlueBarSpacing  = 52;
 

	
 
        private static readonly int[] BLUE_BAR_SPACING =
 
        {
 
            52, // tiny
 
            52, // small
 
            52, // medium
 
            52, // large
 
            52 // huge
 
        };
 
        // width to test on ends of swatch (10% on either end)
 
        private static readonly int[] SWATCH_TEST_WIDTH =
 
        {
 
            26, // tiny
 
            26, // small
 
            26, // medium
 
            28, // large
 
            31  // huge
 
        };
 
        private const int MinSwatchWidth = 306;
 
        private const int MaxSwatchWidth = 411;
 

	
 
        private static ReactionScannerService? s_instance;
 
        public static ReactionScannerService Instance => s_instance ??= new ReactionScannerService();
 

	
 
        private int _swatchHeight    = SWATCH_HEIGHT[(int)Constants.DefaultInterfaceSize];
 
        private int _swatchWidth     = SWATCH_WIDTH[(int)Constants.DefaultInterfaceSize];
 
        private int _swatchTestWidth = SWATCH_TEST_WIDTH[(int)Constants.DefaultInterfaceSize];
 
        private int _colorBarWidth   = COLOR_BAR_WIDTH[(int)Constants.DefaultInterfaceSize];
 
        private int _redBarSpacing   = RED_BAR_SPACING[(int)Constants.DefaultInterfaceSize];
 
        private int _greenBarSpacing = GREEN_BAR_SPACING[(int)Constants.DefaultInterfaceSize];
 
        private int _blueBarSpacing  = BLUE_BAR_SPACING[(int)Constants.DefaultInterfaceSize];
 
        private int _swatchHeight    = 0;
 
        private int _minSwatchWidth  = 0;
 
        private int _maxSwatchWidth  = 0;
 
        private int _swatchWidth     = 0;
 
        private int _redBarSpacing   = 0;
 
        private int _greenBarSpacing = 0;
 
        private int _blueBarSpacing  = 0;
 
        private int _swatchTopBorder    = 0;
 
        private int _swatchBottomBorder = 0;
 
        private int _swatchLeftBorder   = 0;
 
        private int _swatchRightBorder  = 0;
 
        private int _swatchTestWidth = 0;
 

	
 
        private bool _lockedSwatchWidth = false;
 

	
 
        // Current Status
 
        public bool IsCaptured { get; private set; }
 

	
 
        private PaintColor _recordedColor = new PaintColor();
 
        private readonly PaintColor _recordedColor = new();
 
        public PaintColor RecordedColor {
 
            get => _recordedColor;
 
            private set => _recordedColor.Set(value);
 
        }
 

	
 
        private int _pixelMultiplier = 1;
 

	
 
        private InterfaceSize _interfaceSize = Constants.DefaultInterfaceSize;
 

	
 
        private bool _firstRun   = true;
 
        private int _lastSwatchX = -1;
 
        private int _lastSwatchY = -1;
 
        private int _screenX     = 0;
 
        private int _screenY     = 0;
 

	
 
        private Bitmap _targetBitmap;
 

	
 
        private LogVerbosity _logVerbosity = LogVerbosity.Normal;
 

	
 
        private CancellationTokenSource? _canceler;
 

	
 
        public StreamWriter? Log { get; set; }
 
        
 
        private void WriteLog(LogVerbosity verbosity, string format, params object[] args)
 
        {
 
            // Dumping this to Debug, for now.
 
            if (verbosity <= _logVerbosity)
 
            {
 
                Debug.WriteLine(format, args);
 
            }
 
            
 
            /*
 
            if ((Log != null) && (verbosity <= _logVerbosity))
 
            {
 
                Log.WriteLine(format, args);
 
            }
 
            */
 
        }
 
        
 
        public ReactionScannerService()
 
        {
 
            RefreshFromSettings();
 
            Debug.Assert(_targetBitmap != null);
 
        }
 

	
 
        public void RefreshFromSettings()
 
        {
 
            SettingsService.Get(SettingKey.PixelMultiplier, out _pixelMultiplier, Constants.DefaultPixelMultiplier);
 
            SettingsService.Get(SettingKey.InterfaceSize, out int interfaceSize, (int)Constants.DefaultInterfaceSize);
 
            if (Enum.IsDefined(typeof(InterfaceSize), interfaceSize))
 
            {
 
                _interfaceSize = (InterfaceSize)interfaceSize;
 
            }
 

	
 
            SettingsService.Get(SettingKey.ScreenX, out _screenX, Constants.DefaultScreenX);
 
            SettingsService.Get(SettingKey.ScreenY, out _screenY, Constants.DefaultScreenY);
 
            
 
            SettingsService.Get(SettingKey.ScreenWidth,  out int screenWidth, Constants.DefaultScreenWidth);
 
            SettingsService.Get(SettingKey.ScreenHeight, out int screenHeight, Constants.DefaultScreenHeight);
 
            
 
            _targetBitmap = new Bitmap(screenWidth, screenHeight);
 

	
 
            UpdateSwatchSizes();
 
        }
 
        
 

	
 
        private void UpdateSwatchSizes()
 
        {
 
            _swatchHeight    = SWATCH_HEIGHT[(int)_interfaceSize]     * _pixelMultiplier;
 
            _swatchWidth     = SWATCH_WIDTH[(int)_interfaceSize]      * _pixelMultiplier;
 
            _colorBarWidth   = COLOR_BAR_WIDTH[(int)_interfaceSize]   * _pixelMultiplier;
 
            _redBarSpacing   = RED_BAR_SPACING[(int)_interfaceSize]   * _pixelMultiplier;
 
            _greenBarSpacing = GREEN_BAR_SPACING[(int)_interfaceSize] * _pixelMultiplier;
 
            _blueBarSpacing  = BLUE_BAR_SPACING[(int)_interfaceSize]  * _pixelMultiplier;
 
            _swatchTestWidth = SWATCH_TEST_WIDTH[(int)_interfaceSize] * _pixelMultiplier;
 
            _swatchWidth     = MinSwatchWidth * _pixelMultiplier;
 
            _swatchHeight    = SwatchHeight    * _pixelMultiplier;
 
            _redBarSpacing   = RedBarSpacing   * _pixelMultiplier;
 
            _greenBarSpacing = GreenBarSpacing * _pixelMultiplier;
 
            _blueBarSpacing  = BlueBarSpacing  * _pixelMultiplier;
 
            _swatchTopBorder    = 2 * _pixelMultiplier;
 
            _swatchBottomBorder = 2 * _pixelMultiplier;
 
            _swatchLeftBorder   = 2 * _pixelMultiplier;
 
            _swatchRightBorder  = 2 * _pixelMultiplier;
 
            _minSwatchWidth = MinSwatchWidth * _pixelMultiplier;
 
            _maxSwatchWidth = MaxSwatchWidth * _pixelMultiplier;
 
            _swatchTestWidth = (int)(0.1f * MinSwatchWidth * _pixelMultiplier);
 
        }
 

	
 
        public void CancelScan()
 
        {
 
            _canceler?.Cancel();
 
        }
 

	
 
        private static class ColorMatcher
 
        {
 
            public static Color Color;
 

	
 
            public static bool IsMatch(Color otherColor)
 
            {
 
                return PixelColor.IsMatch(Color, otherColor);
 
            }
 
        }
 
        
 
        private bool IsPossibleSwatchSlice(Pixels pixels, int x, int y)
 
        {
 
            // 1.) Check if the top pixel is a dark pixel.
 
            if (!PixelColor.IsDark(pixels.ColorAt(x, y))) return false;
 
            
 
            // 2.) grab the swatch color 2 down from top border
 
            ColorMatcher.Color = pixels.ColorAt(x, y + 2);
 
            ColorMatcher.Color = pixels.ColorAt(x, y + _swatchTopBorder);
 

	
 
            // Scan the column from 2 below the top to 3 above the bottom to ensure the color matches
 
            for (int i = 2; i < (_swatchHeight-3); ++i)
 
            for (int i = 2; i < (_swatchHeight - 3); ++i)
 
            {
 
                if (!pixels.DoesPixelMatch(x, y + i, ColorMatcher.IsMatch)) return false;
 
            }
 

	
 
            return true;
 
        }
 

	
 
        private bool IsPossibleSwatchUpperLeft(Pixels pixels, int x, int y)
 
        {
 
            bool result = true;
 

	
 
            int swatchSolidWidth = _swatchWidth - 4;
 
            int swatchSolidHeight = _swatchHeight - 5; // 2 top and 3 bottom pixels are slightly different colors
 
            int swatchSolidLeftX = x + 2;
 
            int swatchSolidTopY = y + 2;
 
            int swatchSolidRightX = swatchSolidLeftX + swatchSolidWidth - 1;
 
            int swatchSolidWidth   = _swatchWidth  - _swatchLeftBorder - _swatchRightBorder;
 
            int swatchSolidHeight  = _swatchHeight - _swatchTopBorder - _swatchBottomBorder;
 
            int swatchSolidLeftX   = x + _swatchLeftBorder;
 
            int swatchSolidTopY    = y + _swatchTopBorder;
 
            int swatchSolidRightX  = swatchSolidLeftX + swatchSolidWidth - 1;
 
            int swatchSolidBottomY = swatchSolidTopY + swatchSolidHeight - 1;
 
            
 
            Color swatchColor = pixels.ColorAt(swatchSolidLeftX, swatchSolidTopY);
 
            ColorMatcher.Color = swatchColor;
 

	
 
            // Check the other 3 corners of the swatch size for color match
 
            Color testColor = Color.Black;
 
            bool upperRightResult = pixels.DoesPixelMatch(swatchSolidRightX, swatchSolidTopY, ColorMatcher.IsMatch);
 
            if (!upperRightResult)
 
            {
 
                testColor = pixels.ColorAt(swatchSolidRightX, swatchSolidTopY);
 
                WriteLog(LogVerbosity.Excessive, "Upper-right mismatch for {8}, {9} - found {0},{1},{2} at {3}, {4} expected {5},{6},{7}", testColor.R, testColor.G, testColor.B, swatchSolidRightX, swatchSolidTopY, swatchColor.R, swatchColor.G, swatchColor.B, x, y);
 
            }
 
            bool lowerLeftResult = pixels.DoesPixelMatch(swatchSolidLeftX, swatchSolidBottomY, ColorMatcher.IsMatch);
 
            if (!lowerLeftResult)
 
            {
 
                testColor = pixels.ColorAt(swatchSolidLeftX, swatchSolidBottomY);
 
                WriteLog(LogVerbosity.Excessive, "Lower-left mismatch for {8}, {9} - found {0},{1},{2} at {3}, {4} expected {5},{6},{7}", testColor.R, testColor.G, testColor.B, swatchSolidLeftX, swatchSolidBottomY, swatchColor.R, swatchColor.G, swatchColor.B, x, y);
 
            }
 
            bool lowerRightResult = pixels.DoesPixelMatch(swatchSolidRightX, swatchSolidBottomY, ColorMatcher.IsMatch);
 
            if (!lowerRightResult)
 
            {
 
                testColor = pixels.ColorAt(swatchSolidRightX, swatchSolidBottomY);
 
                WriteLog(LogVerbosity.Excessive, "Lower-right mismatch for {8}, {9} - found {0},{1},{2} at {3}, {4} expected {5},{6},{7}", testColor.R, testColor.G, testColor.B, swatchSolidRightX, swatchSolidBottomY, swatchColor.R, swatchColor.G, swatchColor.B, x, y);
 
            }
 

	
 
            result &= upperRightResult;
 
            result &= lowerLeftResult;
 
            result &= lowerRightResult;
 
            if (!result)
 
            {
 
                // Box corners test failed
 
                WriteLog(LogVerbosity.High, "Failed to find left edge for potential swatch of color {2}, {3}, {4} at {0}, {1}", x, y, swatchColor.R, swatchColor.G, swatchColor.B);
 
                return false;
 
            }
 
            
 
            // Find the right side of the swatch, if necessary.
 
            if (!_lockedSwatchWidth)
 
            {
 
                int xCutoff = Math.Min(x + _maxSwatchWidth - _swatchRightBorder, _targetBitmap.Width);
 
                int newRight = swatchSolidRightX;
 
                for (int px = swatchSolidRightX; px < xCutoff; px++)
 
                {
 
                    if (pixels.DoesPixelMatch(px, swatchSolidTopY, ColorMatcher.IsMatch) &&
 
                        pixels.DoesPixelMatch(px, swatchSolidBottomY, ColorMatcher.IsMatch))
 
                    {
 
                        newRight = px;
 
                    }
 
                    else break;
 
                }
 

	
 
                swatchSolidRightX = newRight;
 
                _swatchWidth = swatchSolidRightX + _swatchRightBorder - x + _pixelMultiplier;
 
                WriteLog(LogVerbosity.High, "Setting new swatch width of {0}.", _swatchWidth);
 
            }
 

	
 
            // scan down the right and left sides
 
            for (int yOff = 1; yOff < (swatchSolidHeight - 1); ++yOff)
 
            for (int yOff = _pixelMultiplier; yOff < (swatchSolidHeight - _pixelMultiplier); ++yOff)
 
            {
 
                result &= pixels.DoesPixelMatch(swatchSolidLeftX, swatchSolidTopY + yOff, ColorMatcher.IsMatch);
 
                if (!result)
 
                {
 
                    testColor = pixels.ColorAt(swatchSolidLeftX, swatchSolidTopY + yOff);
 
                    break;
 
                }
 
                result &= pixels.DoesPixelMatch(swatchSolidRightX, swatchSolidTopY + yOff, ColorMatcher.IsMatch);
 
                if (!result)
 
                {
 
                    testColor = pixels.ColorAt(swatchSolidRightX, swatchSolidTopY + yOff);
 
                    break;
 
                }
 
            }
 
            if (!result)
 
            {
 
                WriteLog(LogVerbosity.Normal, "Failed to find left/right edges for potential swatch of color {2}, {3}, {4} at {0}, {1} [failed color = {5},{6},{7}]", x, y, swatchColor.R, swatchColor.G, swatchColor.B, testColor.R, testColor.G, testColor.B);
 
                WriteLog(LogVerbosity.Normal, "Failed to find left/right edges for potential swatch of color {2}, {3}, {4} at ({0}, {1}) [failed color = {5},{6},{7}]", x, y, swatchColor.R, swatchColor.G, swatchColor.B, testColor.R, testColor.G, testColor.B);
 
                return false;
 
            }
 
            for (int xOff = 1; xOff < (swatchSolidWidth - 1); ++xOff)
 
            for (int xOff = _pixelMultiplier; xOff < (swatchSolidWidth - _pixelMultiplier); ++xOff)
 
            {
 
                result &= pixels.DoesPixelMatch(swatchSolidLeftX + xOff, swatchSolidTopY, ColorMatcher.IsMatch);
 
                if (!result)
 
                {
 
                    testColor = pixels.ColorAt(swatchSolidLeftX + xOff, swatchSolidTopY);
 
                    break;
 
                }
 
                result &= pixels.DoesPixelMatch(swatchSolidLeftX + xOff, swatchSolidBottomY, ColorMatcher.IsMatch);
 
                if (!result)
 
                {
 
                    testColor = pixels.ColorAt(swatchSolidLeftX + xOff, swatchSolidBottomY);
 
                    break;
 
                }
 
            }
 
            if (!result)
 
            {
 
                WriteLog(LogVerbosity.Normal, "Failed to match upper/lower edges for potential swatch of color {2}, {3}, {4} at {0}, {1} [failed color = {5},{6},{7}]", x, y, swatchColor.R, swatchColor.G, swatchColor.B, testColor.R, testColor.G, testColor.B);
 
                WriteLog(LogVerbosity.Normal, "Failed to match upper/lower edges for potential swatch of color {2}, {3}, {4} at ({0}, {1}) [failed color = {5},{6},{7}]", x, y, swatchColor.R, swatchColor.G, swatchColor.B, testColor.R, testColor.G, testColor.B);
 
                return false;
 
            }
 

	
 

	
 
            // test the left edge for dark pixels -- the bottom-most pixel is bright now
 
            int i = 0;
 
            for (i = 1; result && i < _swatchHeight - 1; ++i)
 
            for (i = _pixelMultiplier; result && i < _swatchHeight - _pixelMultiplier; ++i)
 
            {
 
                result &= pixels.DoesPixelMatch(x, y + i, PixelColor.IsDark);
 
                if (!result) break;
 
            }
 
            if (!result)
 
            {
 
                // No dark border on the left side
 
                WriteLog(LogVerbosity.Normal, "Failed to find left border for potential swatch of color {2}, {3}, {4} at {0}, {1}", x, y, swatchColor.R, swatchColor.G, swatchColor.B);
 
                return false;
 
            }
 

	
 
            // test the dark top border and for papyrus above and below the swatch
 
            // test the dark top border and for background above and below the swatch
 
            bool borderError = false;
 
            int papyErrorCount = 0;
 
            for (i = 0; result && (i < _swatchWidth - 1); ++i)
 
            int bgErrorCount = 0;
 
            for (i = 0; result && (i < _swatchWidth - _pixelMultiplier); ++i)
 
            {
 
                bool isBorder = pixels.DoesPixelMatch(x + i, y, PixelColor.IsDark);
 
                result &= isBorder;
 
                if (!isBorder)
 
                {
 
                    WriteLog(LogVerbosity.Normal, "Probable swatch at {0},{1} failed upper border test at {2},{3}", x, y, x + i, y);
 
                    borderError = true;
 
                }
 

	
 
                // Checking along the top of the swatch for papyrus
 
                // Checking along the top of the swatch for background
 
                // The row just above is shaded, so check 2 above
 
                if (y > 1)
 
                {
 
                    bool isPapyrus = pixels.DoesPixelMatch(x + i, y - 2, PixelColor.IsPapyrus);
 
                    papyErrorCount += isPapyrus ? 0 : 1;
 
                    bool isUiBackground = pixels.DoesPixelMatch(x + i, y - _swatchTopBorder, PixelColor.IsUiBackground);
 
                    bgErrorCount += isUiBackground ? 0 : 1;
 

	
 
                }
 
                else
 
                {
 
                    ++papyErrorCount;
 
                    ++bgErrorCount;
 
                }
 

	
 
                // Checking along the bottom of the swatch for papyrus
 
                // Checking along the bottom of the swatch for background
 
                if (y < pixels.Height)
 
                {
 
                    bool isPapyrus = pixels.DoesPixelMatch(x + i, y + _swatchHeight, PixelColor.IsPapyrus);
 
                    papyErrorCount += isPapyrus ? 0 : 1;
 
                    bool isUiBackground = pixels.DoesPixelMatch(x + i, y + _swatchHeight, PixelColor.IsUiBackground);
 
                    bgErrorCount += isUiBackground ? 0 : 1;
 
                }
 
                else
 
                {
 
                    ++papyErrorCount;
 
                    ++bgErrorCount;
 
                }
 
            }
 

	
 
            result &= (papyErrorCount < (_swatchWidth / 20)); // allow up to 5% error rate checking for papy texture, because this seems to be inconsistent
 
            if (!result && ((i > (_swatchWidth*0.8)) || (papyErrorCount >= (_swatchWidth/20))))
 
            result &= (bgErrorCount < (_swatchWidth / 20)); // allow up to 5% error rate checking for background texture, because this seems to be inconsistent
 
            if (!result && ((i > (_swatchWidth * 0.8)) || (bgErrorCount >= (_swatchWidth / 20))))
 
            {
 
                if (!borderError && (papyErrorCount < _swatchWidth))
 
                if (!borderError && (bgErrorCount < _swatchWidth))
 
                {
 
                    WriteLog(LogVerbosity.Normal, "Found a potential swatch candidate of width {0} at {1},{2} that had {3} failures matching papyrus texture", i, x, y, papyErrorCount);
 
                    WriteLog(LogVerbosity.Normal, "Found a potential swatch candidate of width {0} at {1},{2} that had {3} failures matching background color", i, x, y, bgErrorCount);
 
                }
 
            }
 

	
 
            return result;
 
        }
 

	
 
        private bool TestPosition(int x, int y, Pixels pixels, ref PaintColor reactedColor)
 
        {
 
            // Check 4 corners of solid area and left/right solid bar areas
 
            bool foundSwatch = IsPossibleSwatchUpperLeft(pixels, x, y); // ((pixel_r < 0x46) && (pixel_g < 0x46) && (pixel_b < 0x46));
 
            bool foundSwatch = IsPossibleSwatchUpperLeft(pixels, x, y);
 
            if (foundSwatch)
 
            {
 
                WriteLog(LogVerbosity.Normal, "Found probable swatch at {0},{1} - checking border slices", x, y);
 
                int borderXOffset = 0;
 
                for (borderXOffset = 2; foundSwatch && (borderXOffset < _swatchTestWidth); ++borderXOffset)
 
                for (borderXOffset = _swatchLeftBorder; foundSwatch && (borderXOffset < _swatchTestWidth); ++borderXOffset)
 
                {
 
                    foundSwatch &= IsPossibleSwatchSlice(pixels, x + borderXOffset, y);
 
                    if (!foundSwatch)
 
                    {
 
                        WriteLog(LogVerbosity.Normal, "Failed slice test at {0},{1}", x + borderXOffset, y);
 
                        break;
 
                    }
 
                    foundSwatch &= IsPossibleSwatchSlice(pixels, x + _swatchWidth - borderXOffset, y);
 
                    if (!foundSwatch)
 
                    {
 
                        WriteLog(LogVerbosity.Normal, "Failed slice test at {0},{1}", x + _swatchWidth - borderXOffset, y);
 
                        break;
 
                    }
 
                }
 
            }
 

	
 
            if (!foundSwatch) return false;
 
            if (!foundSwatch)
 
            {
 
                if (!_lockedSwatchWidth)
 
                {
 
                    // Reset our guess at swatch width.
 
                    _swatchWidth = MinSwatchWidth;
 
                }
 
                return false;
 
            }
 
            
 
            // WE FOUND THE SWATCH!
 
            // Now we know where the color bars are.
 
            _lockedSwatchWidth = true;
 
            int redPixelCount = pixels.LengthOfColorAt(x, y + _redBarSpacing, PixelColor.IsRed);
 
            reactedColor.Red = (byte)Math.Round((float)redPixelCount * 255f / (float)_colorBarWidth);
 
            reactedColor.Red = (byte)Math.Round(redPixelCount * 255f / _swatchWidth);
 

	
 
            int greenPixelCount = pixels.LengthOfColorAt(x, y + _greenBarSpacing, PixelColor.IsGreen);
 
            reactedColor.Green = (byte)Math.Round((float)greenPixelCount * 255f / (float)_colorBarWidth);
 
            reactedColor.Green = (byte)Math.Round(greenPixelCount * 255f / _swatchWidth);
 

	
 
            int bluePixelCount = pixels.LengthOfColorAt(x, y + _blueBarSpacing, PixelColor.IsBlue);
 
            reactedColor.Blue = (byte)Math.Round((float)bluePixelCount * 255f / (float)_colorBarWidth);
 
            reactedColor.Blue = (byte)Math.Round(bluePixelCount * 255f / _swatchWidth);
 
            WriteLog(LogVerbosity.Low, "Found the color swatch at {0}, {1}. Color={2} Red={3}px Green={4}px Blue={5}px", x, y, reactedColor, redPixelCount, greenPixelCount, bluePixelCount);
 
            return true;
 
        }
 

	
 
        public async Task<bool> CaptureReactionAsync(IProgress<float> progress)
 
        {
 
            _canceler = new CancellationTokenSource(); // You can't re-use these, sadly.
 
            return await Task.Run(() => CaptureReaction(progress, _canceler.Token));
 
        }
 

	
 
        private bool CaptureReaction(IProgress<float> progress, CancellationToken cancellToken)
 
        {
 
            PaintColor reactedColor = new();
 
            IsCaptured = false;
 
            _recordedColor.Clear();
 

	
 
            using (var g = Graphics.FromImage(_targetBitmap))
 
            {
 
                Debug.WriteLine("Scan starting at [" + _screenX + ", " + _screenY + "]");
 
                g.CopyFromScreen(_screenX, _screenY, 0, 0, _targetBitmap.Size, CopyPixelOperation.SourceCopy);
 
            }
 
            
 
            _targetBitmap.Save(Path.Combine(ProfileManager.CurrentProfile?.Directory ?? "", "screenshot.png"), ImageFormat.Png);
 
            // _targetBitmap.Save(Path.Combine(ProfileManager.CurrentProfile?.Directory ?? "", "screenshot.png"), ImageFormat.Png);
 

	
 
            Pixels pixels = new(_targetBitmap, _pixelMultiplier);
 

	
 
            IsCaptured = false;
 
            if (!_firstRun)
 
            {
 
                // If this is not the first run, let's check the last location, to see if the UI is still there.
 
                if (TestPosition(_lastSwatchX, _lastSwatchY, pixels, ref reactedColor))
 
                {
 
                    IsCaptured = true;
 
                    RecordedColor = reactedColor;
 
                    return true;
 
                }
 
                
 
                _firstRun = true;
 
            }
 

	
 
            SettingsService.Get("Log.Verbosity", out var verbosityIdx, 1);
 
            _logVerbosity = (LogVerbosity)verbosityIdx;
 
            
 
            int patchTestSize = ((_swatchHeight - 5) / 2) - 1;
 
            int patchTestSize = ((_swatchHeight - _swatchTopBorder - _swatchBottomBorder) / 2) - 1;
 

	
 
            SettingsService.Get("ScanArea.Min.X", out var startX, 0);
 
            SettingsService.Get("ScanArea.Min.Y", out var startY, 0);
 
            SettingsService.Get("ScanArea.Max.X", out var endX,   _targetBitmap.Width);
 
            SettingsService.Get("ScanArea.Max.Y", out var endY,   _targetBitmap.Height);
 
            
 
            startX = Math.Max(2, Math.Min(startX, _targetBitmap.Width - 2));
 
            startY = Math.Max(2, Math.Min(startY, _targetBitmap.Height - 2));
 
            endX = Math.Min(_targetBitmap.Width - 2, Math.Max(2, endX)) - _colorBarWidth; //  + patchTestSize;
 
            endX = Math.Min(_targetBitmap.Width  - 2, Math.Max(2, endX)) - _minSwatchWidth; //  + patchTestSize;
 
            endY = Math.Min(_targetBitmap.Height - 2, Math.Max(2, endY)) - (_blueBarSpacing + 10); // + patchTestSize;
 

	
 
            Debug.WriteLine("startX: " + startX + " endX: " + endX + " startY: " + startY + " endY: " + endY);
 
            Debug.WriteLine("startX: " + startX + " endX: " + endX + " startY: " + startY + " endY: " + endY + " with patch test size of " + patchTestSize);
 
            
 
            int xSpan = endX - startX;
 
            int ySpan = endY - startY;
 
            int total = xSpan * ySpan;
 

	
 
            for (int roughX = startX; roughX < endX ; roughX += patchTestSize)
 
            {
 
                int xMark = roughX - startX;
 
                
 
                for (int roughY = startY; roughY < endY; roughY += patchTestSize)
 
                {
 
                    progress.Report((float)((xMark * ySpan) + (roughY - startY)) / total);
 

	
 
                    cancellToken.ThrowIfCancellationRequested();
 

	
 
                    if (!pixels.IsSolidPatchAt(roughX, roughY, patchTestSize, patchTestSize)) continue;
 
                    Color patchColor = pixels.ColorAt(roughX, roughY);
 

	
 
                    WriteLog(LogVerbosity.Excessive, "Found a solid patch of {2},{3},{4} at {0}, {1}", roughX, roughY, patchColor.R, patchColor.G, patchColor.B);
 
                    for (int x = Math.Max(0, roughX - patchTestSize); x < roughX; ++x)
 
                    for (int x = Math.Max(0, roughX - patchTestSize - _swatchLeftBorder + _pixelMultiplier); x < roughX; ++x)
 
                    {
 
                        for (int y = Math.Max(0, roughY - patchTestSize); y < roughY; ++y)
 
                        for (int y = Math.Max(0, roughY - patchTestSize - _swatchTopBorder + _pixelMultiplier); y < roughY; ++y)
 
                        {
 
                            WriteLog(LogVerbosity.Excessive, "Searching for potential swatch at {0},{1} after found square at {2},{3}", x, y, roughX, roughY);
 
                            
 
                            if (!TestPosition(x, y, pixels, ref reactedColor)) continue;
 
                            
 
                            RecordedColor = reactedColor;
 
                            _lastSwatchX  = x;
 
                            _lastSwatchY  = y;
 
                            _firstRun     = false;
 
                            IsCaptured    = true;
 
                            return true;
 
                        }
 
                    }
 
                    WriteLog(LogVerbosity.Excessive, "False-positive patch of color {0},{1},{2} at {3},{4}", patchColor.R, patchColor.G, patchColor.B, roughX, roughY);
 
                }
 
            }
 
            return false;
 
        }
 

	
 

	
 
        public static Reaction Calculate3WayReaction(PlayerProfile profile, PaintColor expectedColor, PaintColor reactedColor, Reagent reagent0, Reagent reagent1, Reagent reagent2)
 
        {
 
            // A 3-reagent reaction.
 
            Reaction? reaction1 = profile.FindReaction(reagent0, reagent1);
 
            Reaction? reaction2 = profile.FindReaction(reagent0, reagent2);
 

	
 
            Debug.Assert(reaction1 != null);
 
            Debug.Assert(reaction2 != null);
 

	
 
            int r = reactedColor.Red   - expectedColor.Red   - reaction1.Red   - reaction2.Red;
 
            int g = reactedColor.Green - expectedColor.Green - reaction1.Green - reaction2.Green;
 
            int b = reactedColor.Blue  - expectedColor.Blue  - reaction1.Blue  - reaction2.Blue;
 
            
 
            return new Reaction(r, g, b);
 
        }
 

	
 

	
 
        
 
        public static Reaction CalculateReaction(PaintColor expectedColor, PaintColor reactedColor)
 
        {
 
            // A 2-reagent reaction.
 
            int r = reactedColor.Red   - expectedColor.Red;
 
            int g = reactedColor.Green - expectedColor.Green;
 
            int b = reactedColor.Blue  - expectedColor.Blue;
 
            return new Reaction(r, g, b);
 
        }
 
    }
 
}
...
 
\ No newline at end of file
Services/SettingKey.cs
Show inline comments
 
namespace DesertPaintCodex.Services
 
{
 
    public static class SettingKey
 
    {
 
        public const string InterfaceSize   = "interfacesize";
 
        public const string PixelMultiplier = "pixelmultiplier";
 
        public const string ScreenIndex     = "screenindex";
 
        public const string ScreenX         = "screenx";
 
        public const string ScreenY         = "screeny";
 
        public const string ScreenWidth     = "screenwidth";
 
        public const string ScreenHeight    = "screenheight";
 
    }
 
}
...
 
\ No newline at end of file
Util/Constants.cs
Show inline comments
 
using DesertPaintCodex.Models;
 

	
 
namespace DesertPaintCodex.Util
 
{
 
    public static class Constants
 
    {
 
        public const InterfaceSize DefaultInterfaceSize = InterfaceSize.Small;
 
        public static int DefaultPixelMultiplier = 1;
 
        public static int DefaultScreenWidth = 1920;
 
        public static int DefaultScreenHeight = 1080;
 
        public static int DefaultScreenX = 0;
 
        public static int DefaultScreenY = 0;
 

	
 
        private static ReactionTest? _stubReactionTest = null;
 

	
 
        public static ReactionTest StubReactionTest
 
        {
 
            get
 
            {
 
                if (_stubReactionTest == null)
 
                {
 
                    _stubReactionTest = new(
 
                        new Reagent("Toad Skin", "ToadSkin", 48, 96, 48), 
 
                        new Reagent("Falcons Bait", "FalconsBait", 128, 240, 224), 
 
                        null, ClipType.None, true);
 
                }
 

	
 
                return _stubReactionTest;
 
            }
 
            
 
        }
 
            
 
            
 
            
 

	
 
    }
 
}
...
 
\ No newline at end of file
Util/PixelColor.cs
Show inline comments
 
using System;
 
using System.Drawing;
 
using DesertPaintCodex.Services;
 

	
 
namespace DesertPaintCodex.Util
 
{
 
    public class PixelColor
 
    {
 
        private const int ColorTolerance = 3;
 
        private const int ColorTolerance = 2;
 
        
 
        public static bool IsMatch(Color colorA, Color colorB)
 
        {
 
            return ((Math.Abs(colorA.R - colorB.R) <= ColorTolerance) &&
 
                (Math.Abs(colorA.G - colorB.G) <= ColorTolerance) &&
 
                (Math.Abs(colorA.B - colorB.B) <= ColorTolerance));
 
        }
 

	
 
        public static bool IsDark(Color color)
 
        {
 
            return (color.R < 0x47) && (color.G < 0x47) && (color.B < 0x47);
 
        }
 

	
 
        public static bool IsPapyrus(Color color)
 
        public static bool IsUiBackground(Color color)
 
        {
 
            // red between 208 and 244
 
            // 240 and 255
 
            // green between 192 and 237
 
            // 223 and 250
 
            // blue between 145 and 205
 
            // 178 and 232
 
            //return ((r > 0xD0) && (g >= 0xC0) && (b >= 0x91)) &&
 
            //       ((r < 0xF4) && (g <= 0xED) && (b <= 0xCD));
 
            return ((color.R >= 0xD0) && (color.R <= 0xFF) && (color.G >= 0xC0) && (color.G <= 0xFA) && (color.B >= 0x91) && (color.B <= 0xE8));
 
            return (color.R is >= 0x27 and <= 0x2F) && (color.G is >= 0x3B and <= 0x42) && (color.B is >= 0x41 and <= 0x48);
 
        }
 

	
 
        public static bool IsRed(Color color)
 
        {
 
            return (color.R > 0x9F) && (color.G < 0x70) && (color.B < 0x70);
 
            return (color.R > 0x9D) && (color.G < 0x60) && (color.B < 0x60);
 
        }
 
        public static bool IsGreen(Color color)
 
        {
 
            return (color.R < 0x70) && (color.G > 0x9F) && (color.B < 0x70);
 
            return (color.R < 0x60) && (color.G > 0x9D) && (color.B < 0x60);
 
        }
 
        public static bool IsBlue(Color color)
 
        {
 
            return (color.R < 0x70) && (color.G < 0x70) && (color.B > 0x9F);
 
            return (color.R < 0x60) && (color.G < 0x60) && (color.B > 0x9D);
 
        }
 

	
 
    }
 
}
...
 
\ No newline at end of file
ViewModels/ExperimentLogViewModel.cs
Show inline comments
 
using System;
 
using System.Collections.Generic;
 
using System.Collections.ObjectModel;
 
using System.Diagnostics;
 
using ReactiveUI;
 
using DesertPaintCodex.Services;
 
using DesertPaintCodex.Models;
 
using DesertPaintCodex.Util;
 
using DynamicData.Binding;
 

	
 
namespace DesertPaintCodex.ViewModels
 
{
 
    public class ExperimentLogViewModel : ViewModelBase
 
    {
 
        public ObservableCollection<ReactionTest> RemainingTests { get; } = new();
 
        public ObservableCollection<ReactionTest> CompletedTests { get; } = new();
 

	
 
        private int _selectedList = 0;
 
        public int SelectedList { get => _selectedList; private set => this.RaiseAndSetIfChanged(ref _selectedList, value); }
 

	
 
        private int _selectedRemainingTest = 0;
 
        public int SelectedRemainingTest { get => _selectedRemainingTest; private set => this.RaiseAndSetIfChanged(ref _selectedRemainingTest, value); }
 
        
 
        private int _selectedCompletedTest = 0;
 
        public int SelectedCompletedTest { get => _selectedCompletedTest; private set => this.RaiseAndSetIfChanged(ref _selectedCompletedTest, value); }
 

	
 
        private ReactionTestViewModel _testView;
 
        public ReactionTestViewModel TestView { get => _testView; private set => this.RaiseAndSetIfChanged(ref _testView, value); }
 
        
 
        private readonly List<ObservableCollection<ReactionTest>> _testLists = new();
 

	
 
        public ExperimentLogViewModel()
 
        {
 
            PlayerProfile? profile = ProfileManager.CurrentProfile;
 
            Debug.Assert(profile != null);
 
            
 
            _testLists.Add(RemainingTests);
 
            _testLists.Add(CompletedTests);
 
            
 
            ReactionTestService.PopulateRemainingTests(RemainingTests);
 
            ReactionTestService.PopulateCompletedTests(CompletedTests);
 
            
 
            // If we have no remaining tests, switch to the completed list.
 
            _testView = new ReactionTestViewModel();
 
            if (RemainingTests.Count > 0)
 
            {
 
                SelectedList = 0;
 
                _testView.ReactionTest = RemainingTests[0];
 
            }
 
            else
 
            {
 
                SelectedList = 1;
 
                _testView.ReactionTest = CompletedTests[0];
 
            }
 

	
 
            this.WhenAnyPropertyChanged(nameof(SelectedList), nameof(SelectedRemainingTest), nameof(SelectedCompletedTest))
 
                .Subscribe(_ => _testView.ReactionTest = GetSelectedReactionTest());
 

	
 
            _testView.ReactionTest = GetSelectedReactionTest();
 

	
 
            TestView.SaveReaction.Subscribe(_ => OnSaveReaction());
 
            TestView.ClearReaction.Subscribe(_ => OnClearReaction());
 
            TestView.FinalizeTestResults.Subscribe(_ => OnReactionResults());
 
        }
 

	
 
        private ReactionTest GetSelectedReactionTest()
 
        {
 
            int itemIndex = (SelectedList == 0) ? SelectedRemainingTest : SelectedCompletedTest;
 
            if (itemIndex < 0) return Constants.StubReactionTest;
 
            var list = _testLists[SelectedList];
 
            return (itemIndex >= list.Count) ? Constants.StubReactionTest : list[itemIndex];
 
        }
 
        
 
        #region Command Handlers
 

	
 
        private void OnSaveReaction()
 
        {
 
            ReactionTest test = TestView.ReactionTest;
 
            int oldPos = RemainingTests.IndexOf(test);
 
            
 
            // Move test to Completed Tests.
 
            InsertTestIntoList(test, CompletedTests);
 
            RemainingTests.RemoveAt(oldPos);
 
            
 
            // Select next Remaining Test.
 
            if (RemainingTests.Count == 0) return;
 
            SelectedRemainingTest = (oldPos == RemainingTests.Count) ? RemainingTests.Count - 1 : oldPos;
 

	
 
            // If we have just inserted our first completed test, select it.
 
            if (CompletedTests.Count == 1)
 
            {
 
                 SelectedCompletedTest = 0;
 
            }
 
        }
 

	
 
        private void OnClearReaction()
 
        {
 
            ReactionTest test = TestView.ReactionTest;
 
            if (!CompletedTests.Contains(test)) return;
 

	
 
            int oldPos = CompletedTests.IndexOf(test);
 
            
 
            // Move test to Remaining Tests.
 
            InsertTestIntoList(test, RemainingTests);
 
            CompletedTests.RemoveAt(oldPos);
 
            
 
            // Select next Completed Test.
 
            if (CompletedTests.Count == 0) return;
 
            SelectedCompletedTest = (oldPos == CompletedTests.Count) ? CompletedTests.Count - 1 : oldPos;
 

	
 
            // If we have just inserted our first remaining test, select it.
 
            if (RemainingTests.Count == 1)
 
            {
 
                SelectedRemainingTest = 0;
 
            }
 
        }
 

	
 
        private void OnReactionResults()
 
        {
 
            ReactionTest? test = null;
 
            int selectedTest = SelectedRemainingTest;
 
            if (selectedTest >= 0)
 
            {
 
                test = RemainingTests[selectedTest];
 
            }
 
            ReactionTestService.PopulateRemainingTests(RemainingTests);
 
            if (test != null)
 
            {
 
                int newIndex = RemainingTests.IndexOf(test);
 
                SelectedRemainingTest = newIndex;
 
            }
 
        }
 
        
 
        #endregion
 

	
 
        private static void InsertTestIntoList(ReactionTest test, IList<ReactionTest> list)
 
        {
 
            int i;
 
            for (i = 0; i < list.Count; i++)
 
            {
 
                if (test.CompareTo(list[i]) < 0) break;
 
            }
 
            list.Insert(i, test);
 
        }
 
    }
 
}
ViewModels/ReactionTestViewModel.cs
Show inline comments
 
using System;
 
using System.Collections.Generic;
 
using System.Collections.ObjectModel;
 
using System.Diagnostics;
 
using System.Linq;
 
using System.Reactive;
 
using System.Reactive.Linq;
 
using System.Threading.Tasks;
 
using DesertPaintCodex.Models;
 
using DesertPaintCodex.Services;
 
using DesertPaintCodex.Util;
 
using ReactiveUI;
 
using DynamicData;
 

	
 
namespace DesertPaintCodex.ViewModels
 
{
 
    public class ReactionTestViewModel : ViewModelBase
 
    {
 
        private ReactionTest _reactionTest = Constants.StubReactionTest;
 
        public ReactionTest ReactionTest
 
        {
 
            get => _reactionTest;
 
            set => this.RaiseAndSetIfChanged(ref _reactionTest, value);
 
        }
 

	
 
        private readonly List<Reagent> _allPigmentList = new();
 
        public ObservableCollection<Reagent> BufferPigmentList { get; } = new();
 

	
 

	
 
        public ReactionTestViewModel()
 
        {
 
            List<string> reagentNames = ReagentService.Names;
 
            
 
            foreach (var reagent in reagentNames.Select(ReagentService.GetReagent).Where(x => !x.IsCatalyst))
 
            {
 
                _allPigmentList.Add(reagent);
 
            }
 

	
 
            this.WhenAnyValue(x => x.ReactionTest)
 
                .Subscribe(_ => UpdateDerivedState());
 
            
 
            ShowScreenSettingsDialog = new Interaction<ScreenSettingsViewModel, Unit>();
 
            SaveReaction = ReactiveCommand.Create(() => ReactionTest.SaveReaction());
 
            ClearReaction = ReactiveCommand.Create(() => ReactionTest.ClearReaction());
 
            FinalizeTestResults = ReactiveCommand.Create(Test);
 
        }
 

	
 
        private void UpdateDerivedState()
 
        {
 
            // There are more "reactive" ways to pull this off, but I don't have time to figure out all those recipes
 
            // right now.
 
            SetupTest();
 
        }
 
            
 
        
 
        private void SetupTest()
 
        {
 
            if (ReactionTest.Requires3Way)
 
            {
 
                FilterBufferPigments();
 
            }
 
        }
 

	
 
        private void FilterBufferPigments()
 
        {
 
            // Avalonia doesn't really have proper filtering for listy controls yet.
 
            BufferPigmentList.Clear();
 

	
 
            BufferPigmentList.AddRange(_allPigmentList.Where(pigment =>
 
                    pigment != ReactionTest.Reagent1 && pigment != ReactionTest.Reagent2));
 
        }
 

	
 
        public async void Analyze()
 
        {
 
            Debug.WriteLine("Analyze");
 
            try
 
            {
 
                await ReactionTest.StartScan();
 
                await FinalizeTestResults.Execute();
 
            }
 
            catch (OperationCanceledException)
 
            {
 
                Debug.WriteLine("Scan canceled.");
 
            }
 
        }
 

	
 
        public void MarkInert()
 
        {
 
            ReactionTest.MarkInert();
 
        }
 

	
 
        public void CancelScan()
 
        {
 
            ReactionTest.CancelScan();
 
        }
 

	
 
        public async Task OpenScreenSettings()
 
        {
 
            await ShowScreenSettingsDialog.Handle(new ScreenSettingsViewModel());
 
        }
 

	
 
        public async Task ShowClipInfo()
 
        {
 
            await ShowInfoBox("Clipped Reactions", "The Pigment Lab is only capable of displaying color channel values in the 0-255 range. However, sometimes, your reactions will push one or more channels outside of that range. When that happens, the value will be clamped either to 0 or 255. In this case, we say that the reaction was \"Clipped.\"\n\nThis is nothing to panic about, though. We solve this issue by using a third (buffer) reagent to offset the extreme value, so that it falls within measurable range, similar to how we test Catalyst+Catalyst reactions. Desert Paint Codex will automatically do the math for this, but it does move clipped reactions towards the end of your test list, because you'll want any reactions between the buffer reagent and your two test reagents already known, prior to running your buffered test.");
 
        }
 

	
 
        private void Test()
 
        {
 
            Debug.WriteLine("Test complete");
 
        }
 
        
 
        public Interaction<ScreenSettingsViewModel, Unit> ShowScreenSettingsDialog { get; }
 

	
 
        public ReactiveCommand<Unit, Unit> ClearReaction { get; }
 
        public ReactiveCommand<Unit, Unit> SaveReaction { get; }
 
        public ReactiveCommand<Unit, Unit> FinalizeTestResults { get; }
 
    }
 
}
...
 
\ No newline at end of file
ViewModels/ScreenSettingsViewModel.cs
Show inline comments
 

 
using Avalonia.Controls;
 
using Avalonia.Platform;
 
using DesertPaintCodex.Models;
 
using DesertPaintCodex.Services;
 
using ReactiveUI;
 
using System.Collections.Generic;
 
using System.Collections.ObjectModel;
 
using System.Diagnostics;
 
using System.Reactive;
 
using Avalonia;
 
using Avalonia.Controls.ApplicationLifetimes;
 
using DesertPaintCodex.Util;
 

	
 
namespace DesertPaintCodex.ViewModels
 
{
 
    public class ScreenSettingsViewModel : ViewModelBase
 
    {
 
        public ObservableCollection<string> InterfaceSizes { get; } = new();
 
        public ObservableCollection<ScreenMetrics> Screens { get; } = new();
 

	
 
        private int _selectedInterfaceSize;
 
        public int SelectedInterfaceSize { get => _selectedInterfaceSize; private set => this.RaiseAndSetIfChanged(ref _selectedInterfaceSize, value); }
 

	
 
        
 
        private int _pixeMultiplier;
 
        public int PixelMultiplier { get => _pixeMultiplier; private set => this.RaiseAndSetIfChanged(ref _pixeMultiplier, value); }
 

	
 
        private int _screenIndex;
 
        public int ScreenIndex { get => _screenIndex; private set => this.RaiseAndSetIfChanged(ref _screenIndex, value); }
 
        
 
        public ScreenSettingsViewModel()
 
        {
 
            if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
 
            {
 
                IScreenImpl screen = desktop.MainWindow.PlatformImpl.Screen;
 
                for (int i = 0; i < (int)InterfaceSize.Count; i++)
 
                {
 
                    InterfaceSizes.Add(((InterfaceSize)i).ToString());
 
                }
 

	
 
                IReadOnlyList<Screen> screens = screen.AllScreens;
 

	
 
                for (int i = 0; i < screens.Count; i++)
 
                {
 
                    Screens.Add(new ScreenMetrics(screens[i].Bounds, screens[i].Primary));
 
                }
 
            
 
                SettingsService.Get(SettingKey.InterfaceSize,   out _selectedInterfaceSize, (int)Constants.DefaultInterfaceSize);
 
                
 
                SettingsService.Get(SettingKey.PixelMultiplier, out _pixeMultiplier, Constants.DefaultPixelMultiplier);
 
                SettingsService.Get(SettingKey.ScreenIndex,     out _screenIndex, 0);
 

	
 
                _screenIndex = System.Math.Min(_screenIndex, screens.Count - 1); // In case the user has fewer screens than last time.
 

	
 
                Save = ReactiveCommand.Create(() => {
 
                    SettingsService.Set(SettingKey.InterfaceSize,   _selectedInterfaceSize);
 
                    SettingsService.Set(SettingKey.PixelMultiplier, _pixeMultiplier);
 
                    SettingsService.Set(SettingKey.ScreenIndex,     _screenIndex);
 
                    SettingsService.Set(SettingKey.ScreenX,         screens[_screenIndex].Bounds.X);
 
                    SettingsService.Set(SettingKey.ScreenY,         screens[_screenIndex].Bounds.Y);
 
                    SettingsService.Set(SettingKey.ScreenWidth,     screens[_screenIndex].Bounds.Width);
 
                    SettingsService.Set(SettingKey.ScreenHeight,    screens[_screenIndex].Bounds.Height);
 

	
 
                    SettingsService.Save();
 
                
 
                    ReactionScannerService.Instance.RefreshFromSettings();
 
                });
 
            }
 
            else
 
            {
 
                // Placeholder stuff.
 
                
 
                for (int i = 0; i < (int)InterfaceSize.Count; i++)
 
                {
 
                    InterfaceSizes.Add(((InterfaceSize)i).ToString());
 
                }
 
            
 
                Screens.Add(new ScreenMetrics(new Avalonia.PixelRect(0, 0, 1920, 1080), true));
 
                Screens.Add(new ScreenMetrics(new Avalonia.PixelRect(1920, 0, 1920, 1080), false));
 
            
 
                SelectedInterfaceSize = 0;
 
                
 
                PixelMultiplier       = 1;
 
                ScreenIndex           = 0;
 
                
 
                Save = ReactiveCommand.Create(() => { });
 
            }
 
        }
 

	
 
        public ReactiveCommand<Unit, Unit> Save { get; }
 
    }
 
}
ViewModels/SelectProfileViewModel.cs
Show inline comments
 
using System.Reactive;
 
using ReactiveUI;
 
using System.Collections.Generic;
 
using System.Collections.ObjectModel;
 
using System.Diagnostics;
 
using DesertPaintCodex.Models;
 
using DesertPaintCodex.Services;
 
using DynamicData;
 

	
 
namespace DesertPaintCodex.ViewModels
 
{
 
    public class SelectProfileViewModel : ViewModelBase
 
    {
 
        public List<string> Profiles { get; private set; }
 
        public ObservableCollection<string> Profiles { get; } = new();
 

	
 
        private string? _selectedItem;
 
        public string? SelectedItem { get => _selectedItem; set => this.RaiseAndSetIfChanged(ref _selectedItem, value); }
 
        
 
        public SelectProfileViewModel()
 
        {
 
            Profiles = ProfileManager.GetProfileList();
 
            Profiles.AddRange(ProfileManager.GetProfileList());
 

	
 
            var okEnabled = this.WhenAnyValue(
 
                x => x.SelectedItem,
 
                x => !string.IsNullOrWhiteSpace(x));
 
            
 
            NewProfile = ReactiveCommand.Create(() => { });
 

	
 
            Ok = ReactiveCommand.Create(() =>
 
            {
 
                Debug.Assert(_selectedItem != null);
 
                ProfileManager.LoadProfile(_selectedItem);
 
            }, okEnabled);
 

	
 
            Cancel = ReactiveCommand.Create(() => { });
 

	
 
            if (Profiles.Count > 0)
 
            {
 
                SelectedItem = Profiles[0];
 
            }
 
        }
 
        
 
        public ReactiveCommand<Unit, Unit> NewProfile { get; }
 

	
 
        public ReactiveCommand<Unit, Unit> Ok { get; }
 

	
 
        public ReactiveCommand<Unit, Unit> Cancel { get; }
 
    }
 
}
ViewModels/WelcomeViewModel.cs
Show inline comments
 
using System;
 
using System.Reactive;
 
using System.Reactive.Linq;
 
using DesertPaintCodex.Services;
 
using ReactiveUI;
 

	
 
namespace DesertPaintCodex.ViewModels
 
{
 
    public class WelcomeViewModel : ViewModelBase
 
    {
 
        private SelectProfileViewModel? _selectProfileVM;
 
        private CreateProfileViewModel? _createProfileVM;
 
        
 
        private ViewModelBase? _profileActivity;
 
        public ViewModelBase? ProfileActivity  { get => _profileActivity; private set => this.RaiseAndSetIfChanged(ref _profileActivity, value); }
 

	
 
        public WelcomeViewModel()
 
        {
 
            FinishWelcome = ReactiveCommand.Create(() => { });
 
            
 
            if (ProfileManager.HasProfiles())
 
            {
 
                ShowSelectProfile();
 
            }
 
            else
 
            {
 
                ShowCreateProfile();
 
            }
 
        }
 

	
 
        private void ShowSelectProfile()
 
        {
 
            if (_selectProfileVM == null)
 
            {
 
                _selectProfileVM = new SelectProfileViewModel();
 
                _selectProfileVM.NewProfile.Subscribe(_ => ShowCreateProfile());
 
                Observable.Merge(_selectProfileVM.Ok, _selectProfileVM.Cancel)
 
                    .Take(1)
 
                    .InvokeCommand(FinishWelcome);
 
            }
 

	
 
            _selectProfileVM = new SelectProfileViewModel();
 
            _selectProfileVM.NewProfile.Subscribe(_ => ShowCreateProfile());
 
            Observable.Merge(_selectProfileVM.Ok, _selectProfileVM.Cancel)
 
                .Take(1)
 
                .InvokeCommand(FinishWelcome);
 
            
 
            ProfileActivity = _selectProfileVM;
 
        }
 

	
 
        private void ShowCreateProfile()
 
        {
 
            if (_createProfileVM == null)
 
            {
 
                _createProfileVM = new CreateProfileViewModel();
 
                Observable.Merge(_createProfileVM.Ok, _createProfileVM.Cancel).Subscribe(_ => ShowSelectProfile());
 
            }
 
            _createProfileVM = new CreateProfileViewModel();
 
            Observable.Merge(_createProfileVM.Ok, _createProfileVM.Cancel).Subscribe(_ => ShowSelectProfile());
 

	
 
            ProfileActivity = _createProfileVM;
 
        }
 

	
 
        public ReactiveCommand<Unit, Unit> FinishWelcome { get; }
 
    }
 
}
...
 
\ No newline at end of file
Views/ExperimentLogView.axaml
Show inline comments
 
<UserControl xmlns="https://github.com/avaloniaui"
 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 
             xmlns:vm="using:DesertPaintCodex.ViewModels"
 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 
             xmlns:views="clr-namespace:DesertPaintCodex.Views"
 
             xmlns:local="clr-namespace:DesertPaintCodex.Models"
 
             mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="500"
 
             x:Class="DesertPaintCodex.Views.ExperimentLogView">
 

	
 
    <Design.DataContext>
 
        <vm:ExperimentLogViewModel/>
 
    </Design.DataContext>
 

	
 
    <UserControl.DataContext>
 
        <vm:ExperimentLogViewModel />
 
    </UserControl.DataContext>
 
    
 
    <UserControl.Styles>
 
        <Style Selector="TabControl.ListFilter">
 
            <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}" />
 
        </Style>
 
        <Style Selector="TabControl.ListFilter > TabItem">
 
            <Setter Property="BorderThickness" Value="0" />
 
            <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}" />
 
            <Setter Property="Foreground" Value="White" />
 
            <Setter Property="FontSize" Value="14" />
 
            <Setter Property="Margin" Value="0" />
 
            <Setter Property="Padding" Value="8" />
 
            <Setter Property="Opacity" Value="0.5" />
 
        </Style>
 

	
 
        <Style Selector="TabControl.ListFilter WrapPanel">
 
            <Setter Property="Background" Value="{DynamicResource FlatBackgroundBrush}"/>
 
        </Style>
 

	
 
        <Style Selector="TabControl.ListFilter > TabItem:pointerover">
 
            <Setter Property="Opacity" Value="1" />
 
        </Style>
 

	
 
        <Style Selector="TabControl.ListFilter > TabItem:selected">
 
            <Setter Property="Opacity" Value="1" />
 
            <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}" />
 
        </Style>
 

	
 
        <Style Selector="ToolTip">
 
            <Setter Property="Padding" Value="20" />
 
        </Style>
 
    </UserControl.Styles>
 
    
 
    <UserControl.DataTemplates>
 
        <DataTemplate DataType="{x:Type local:ReactionTest}">
 
            <Grid ColumnDefinitions="90,12,90,12,16,6,60" RowDefinitions="Auto">
 
                <TextBlock Text="{Binding Reagent1.Name}" Grid.Row="0" Grid.Column="0" />
 
                <TextBlock Text="{Binding Reagent2.Name}" Grid.Row="0" Grid.Column="2" />
 
                <Path Classes="Catalyst" IsVisible="{Binding IsAllCatalysts}"
 
                      Width="16" Height="16" Grid.Row="0" Grid.Column="4" />
 
                <views:ClipArrowsView Grid.Row="0" Grid.Column="6" Flags="{Binding Clipped}"/>
 
            </Grid>
 
        </DataTemplate>
 
    </UserControl.DataTemplates>
 

	
 
    <DockPanel Classes="Activity">
 
        <TabControl DockPanel.Dock="Left" Width="318" Classes="ListFilter" SelectedIndex="{Binding SelectedList}">
 
            <TabItem Header="Pending Tests">
 
                <ListBox Items="{Binding RemainingTests}" SelectedIndex="{Binding SelectedRemainingTest}" />
 
                <ListBox Name="RemainingList"
 
                         Items="{Binding RemainingTests}"
 
                         SelectedIndex="{Binding SelectedRemainingTest}"
 
                         SelectionChanged="RemainingSelectionChanged"/>
 
            </TabItem>
 
            <TabItem Header="Completed Tests">
 
                <ListBox Items="{Binding CompletedTests}" SelectedIndex="{Binding SelectedCompletedTest}" />
 
                <ListBox Items="{Binding CompletedTests}"
 
                         SelectedIndex="{Binding SelectedCompletedTest}" />
 
            </TabItem>
 
        </TabControl>
 
        
 
        <views:ReactionTestView DataContext="{Binding TestView}"/>
 
    </DockPanel>
 
    
 
    
 
</UserControl>
...
 
\ No newline at end of file
Views/ExperimentLogView.axaml.cs
Show inline comments
 
using Avalonia.Markup.Xaml;
 
using Avalonia.Controls;
 
using Avalonia.Markup.Xaml;
 
using Avalonia.ReactiveUI;
 
using DesertPaintCodex.ViewModels;
 

	
 

	
 
namespace DesertPaintCodex.Views
 
{
 
    public class ExperimentLogView : ReactiveUserControl<ExperimentLogViewModel>
 
    {
 
        private readonly ListBox? _remainingList;
 
        
 
        public ExperimentLogView()
 
        {
 
            InitializeComponent();
 

	
 
            _remainingList = this.FindControl<ListBox>("RemainingList");
 
        }
 

	
 
        private void InitializeComponent()
 
        {
 
            AvaloniaXamlLoader.Load(this);
 
        }
 
        
 
        private void RemainingSelectionChanged(object? sender, SelectionChangedEventArgs e)
 
        {
 
            if (_remainingList == null) return;
 
            
 
            foreach (var item in e.AddedItems)
 
            {
 
                _remainingList.ScrollIntoView(item);
 
                return;
 
            }
 
        }
 
    }
 
}
Views/ScreenSettingsView.axaml
Show inline comments
 
<Window xmlns="https://github.com/avaloniaui"
 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 
        xmlns:vm="using:DesertPaintCodex.ViewModels"
 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 
        x:Class="DesertPaintCodex.Views.ScreenSettingsView"
 
        mc:Ignorable="d" d:DesignWidth="310" d:DesignHeight="330"
 
        Width="320" Height="330"
 
        WindowStartupLocation="CenterScreen"
 
        Title="Screen Settings">
 

	
 
  <Design.DataContext>
 
    <vm:ScreenSettingsViewModel/>
 
  </Design.DataContext>
 
  
 
  <DockPanel Margin="20">
 
    <TextBlock DockPanel.Dock="Top" Margin="0, 0, 0, 4">Which screen is the game on?</TextBlock>
 
    
 
    <Grid DockPanel.Dock="Top" ColumnDefinitions="80,10,80,10,90" RowDefinitions="Auto" Margin="7 0 0 0">
 
      <TextBlock Text="Position" Grid.Row="0" Grid.Column="0"/>
 
      <TextBlock Text="Size" Grid.Row="0" Grid.Column="2"/>
 
    </Grid>
 
    
 
    <Button DockPanel.Dock="Bottom" Command="{Binding Save}">Ok</Button>
 
    
 
    <Grid DockPanel.Dock="Bottom" ColumnDefinitions="*,12,80,12,24" RowDefinitions="Auto,30,Auto" Margin="0, 0, 0, 30">
 
      <TextBlock VerticalAlignment="Center" TextWrapping="Wrap" TextAlignment="Right" Grid.Row="0" Grid.Column="0">Game UI Size</TextBlock>
 
      <ComboBox Items="{Binding InterfaceSizes}" SelectedIndex="{Binding SelectedInterfaceSize, Mode=TwoWay}" Grid.Row="0" Grid.Column="2" /> 
 
      <Border Classes="Help" Grid.Row="0" Grid.Column="4">
 
        <TextBlock>?</TextBlock>
 
        <ToolTip.Tip>
 
          <StackPanel Margin="5" Spacing="20" Orientation="Vertical">
 
            <TextBlock Classes="BlockHeader">Game UI Size</TextBlock>
 
            <TextBlock TextWrapping="Wrap">[Coming Soon]</TextBlock>
 
          </StackPanel>
 
        </ToolTip.Tip>
 
      </Border>
 
      
 
      <TextBlock VerticalAlignment="Center" TextWrapping="Wrap" TextAlignment="Right" Grid.Row="2" Grid.Column="0">
 
        Screen pixels to game pixels:
 
      </TextBlock>
 
      <NumericUpDown Minimum="1" Maximum="4" Increment="1" Value="{Binding PixelMultiplier}" Grid.Row="2" Grid.Column="2"/>
 
      <Border Classes="Help" Grid.Row="2" Grid.Column="4">
 
        <TextBlock>?</TextBlock>
 
        <ToolTip.Tip>
 
          <StackPanel Margin="5" Spacing="20" Orientation="Vertical">
 
            <TextBlock Classes="BlockHeader">Screen Pixels to Game Pixels</TextBlock>
 
            <TextBlock TextWrapping="Wrap">[Coming Soon]</TextBlock>
 
            <TextBlock TextWrapping="Wrap">If your screen is scaled at the system-level, you may need to adjust this number to measure UI elements correctly.</TextBlock>
 
          </StackPanel>
 
        </ToolTip.Tip>
 
      </Border></Grid>
 
      </Border>
 
    </Grid>
 

	
 
    <ListBox Items="{Binding Screens}" SelectedIndex="{Binding ScreenIndex, Mode=TwoWay}" Margin="0,4,0,20">
 
      <ListBox.ItemTemplate>
 
        <DataTemplate>
 
          <Grid ColumnDefinitions="80,10,80,10,60" RowDefinitions="Auto">
 
            <TextBlock Text="{Binding PositionToStr}" Grid.Row="0" Grid.Column="0" />
 
            <TextBlock Text="{Binding SizeToStr}"  Grid.Row="0" Grid.Column="2"/>
 
            <TextBlock Text="Primary" IsVisible="{Binding IsPrimary}" Grid.Row="0" Grid.Column="4" />
 
          </Grid>
 
        </DataTemplate>
 
      </ListBox.ItemTemplate>
 
    </ListBox>
 
    
 
  </DockPanel>
 

	
 
</Window>
0 comments (0 inline, 0 general)