Changeset - f1d4569c0495
[Not reviewed]
default
0 4 0
Tess Snider (Malkyne) - 3 years ago 2021-07-24 10:26:20
this@malkyne.org
3 way reactions should now allow you to change the position of the buffer
reagent from first to last.
4 files changed with 183 insertions and 44 deletions:
0 comments (0 inline, 0 general)
Models/ReactionTest.cs
Show inline comments
 
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
 
        private Reagent? _bufferReagentFirst;
 
        public Reagent? BufferReagentFirst
 
        {
 
            get => _bufferReagent;
 
            get => _bufferReagentFirst;
 
            set
 
            {
 
                if (_bufferReagentFirst == value) return;
 
                _bufferReagentFirst = value;
 
                
 
                UpdateRecipe();
 
                
 
                NotifyPropertyChanged(nameof(BufferReagentFirst));
 
                NotifyPropertyChanged(nameof(CanScan));
 
            }
 
        }
 
        
 
        private Reagent? _bufferReagentLast;
 
        public Reagent? BufferReagentLast
 
        {
 
            get => _bufferReagentLast;
 
            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));
 
                if (_bufferReagentLast == value) return;
 
                _bufferReagentLast = value;
 

	
 
                UpdateRecipe();
 

	
 
                NotifyPropertyChanged(nameof(BufferReagentLast));
 
                NotifyPropertyChanged(nameof(CanScan));
 
                UpdateHypotheticalColor();
 
            }
 
        }
 

	
 
        private bool _useFirstBuffer;
 

	
 
        public bool UseFirstBuffer
 
        {
 
            get => _useFirstBuffer;
 
            set
 
            {
 
                _useFirstBuffer = value;
 
                UpdateRecipe();
 
                NotifyPropertyChanged(nameof(UseFirstBuffer));
 
                NotifyPropertyChanged(nameof(CanScan));
 
                NotifyPropertyChanged(nameof(ShowFirstBuffer));
 
                NotifyPropertyChanged(nameof(ShowLastBuffer));
 
            }
 
        }
 

	
 
        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));
 
                NotifyPropertyChanged(nameof(CanPickBuffer));
 
                NotifyPropertyChanged(nameof(ShowFirstBuffer));
 
                NotifyPropertyChanged(nameof(ShowLastBuffer));
 
            }
 
        }
 

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

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

	
 
        public bool HasResults => (ObservedColor != null) && (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 bool CanPickBuffer => (State == TestState.ClippedResult) || IsAllCatalysts;
 

	
 
        public bool ShowFirstBuffer => Requires3Way && UseFirstBuffer;
 

	
 
        public bool ShowLastBuffer => Requires3Way && !UseFirstBuffer;
 

	
 

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

	
 
        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;
 
            UseFirstBuffer = true;
 
            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;
 
            UpdateHypotheticalColor();
 
        }
 

	
 
        
 
        #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)
 
                {
 
                    // Handle the normal case.
 
                    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();
 
                    }
 
                    
 
                    PlayerProfile? profile = ProfileManager.CurrentProfile;
 
                    profile?.SetPairClipStatus(Reagent1, Reagent2, Clipped);
 
                }
 
            }
 
            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);
 
            profile.SetPairClipStatus(Reagent1, Reagent2, ClipType.None);
 
            if (State == TestState.Saved)
 
            {
 
                profile.Save();
 
            }
 
            
 
            Reaction = null;
 
            BadReaction = null;
 
            Clipped = ClipType.None;
 
            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 (BufferReagent != null)
 
            if (_useFirstBuffer)
 
            {
 
                return ReactionScannerService.Calculate3WayReaction(ProfileManager.CurrentProfile, HypotheticalColor,
 
                    ObservedColor, BufferReagent, Reagent1, Reagent2);
 
                if (BufferReagentFirst != null)
 
                {
 
                    return ReactionScannerService.Calculate3WayReaction(ProfileManager.CurrentProfile, HypotheticalColor,
 
                        ObservedColor, BufferReagentFirst, Reagent1, Reagent2, true);
 
                }
 
            }
 
            else
 
            {
 
                if (BufferReagentLast != null)
 
                {
 
                    return ReactionScannerService.Calculate3WayReaction(ProfileManager.CurrentProfile, HypotheticalColor,
 
                        ObservedColor, Reagent1, Reagent2, BufferReagentLast, false);
 
                }
 
            }
 
            
 
            return ReactionScannerService.CalculateReaction(HypotheticalColor, ObservedColor);
 
        }
 

	
 

	
 
        private void UpdateHypotheticalColor()
 
        {
 
            HypotheticalColor = null;
 
            HypotheticalColor = (IsAllCatalysts && (BufferReagent == null)) ? null : _recipe.BaseColor;
 
            HypotheticalColor = (IsAllCatalysts && !BufferIsSelected) ? null : _recipe.BaseColor;
 
        }
 

	
 
        private bool BufferIsSelected => UseFirstBuffer ? (BufferReagentFirst != null) : (BufferReagentLast != null);
 

	
 

	
 
        private void UpdateRecipe()
 
        {
 
            _recipe.Clear();
 
            if (_useFirstBuffer)
 
            {
 
                if (_bufferReagentFirst == null)
 
                {
 
                    _recipe.AddReagent(Reagent1.Name);
 
                    _recipe.AddReagent(Reagent2.Name);
 
                }
 
                else
 
                {
 
                    _recipe.AddReagent(_bufferReagentFirst.Name);
 
                    _recipe.AddReagent(Reagent1.Name);
 
                    _recipe.AddReagent(Reagent2.Name);
 
                } 
 
            }
 
            else
 
            {
 
                if (_bufferReagentLast == null)
 
                {
 
                    _recipe.AddReagent(Reagent1.Name);
 
                    _recipe.AddReagent(Reagent2.Name);
 
                }
 
                else
 
                {
 
                    _recipe.AddReagent(Reagent1.Name);
 
                    _recipe.AddReagent(Reagent2.Name);
 
                    _recipe.AddReagent(_bufferReagentLast.Name);
 
                }                
 
            }
 
            
 
            UpdateHypotheticalColor();
 
        }
 
        
 
        #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)
 
            };
 
        }
 
    }
 
}
Services/ReactionScannerService.cs
Show inline comments
...
 
@@ -284,218 +284,229 @@ namespace DesertPaintCodex.Services
 
            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 background
 
                // The row just above is shaded, so check 2 above
 
                if (y > 1)
 
                {
 
                    bool isUiBackground = pixels.DoesPixelMatch(x + i, y - _swatchTopBorder, PixelColor.IsUiBackground);
 
                    bgErrorCount += isUiBackground ? 0 : 1;
 

	
 
                }
 
                else
 
                {
 
                    ++bgErrorCount;
 
                }
 

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

	
 
            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 && (bgErrorCount < _swatchWidth))
 
                {
 
                    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);
 
            if (foundSwatch)
 
            {
 
                WriteLog(LogVerbosity.Normal, "Found probable swatch at {0},{1} - checking border slices", x, y);
 
                int borderXOffset = 0;
 
                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)
 
            {
 
                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(redPixelCount * 255f / _swatchWidth);
 

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

	
 
            int bluePixelCount = pixels.LengthOfColorAt(x, y + _blueBarSpacing, PixelColor.IsBlue);
 
            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);
 

	
 
            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 - _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)) - _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 + " 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 - _swatchLeftBorder + _pixelMultiplier); x < roughX; ++x)
 
                    {
 
                        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)
 
        public static Reaction Calculate3WayReaction(PlayerProfile profile, PaintColor expectedColor, PaintColor reactedColor, Reagent reagent0, Reagent reagent1, Reagent reagent2, bool firstBuffer)
 
        {
 
            // A 3-reagent reaction.
 
            Reaction? reaction1 = profile.FindReaction(reagent0, reagent1);
 
            Reaction? reaction2 = profile.FindReaction(reagent0, reagent2);
 
            Reaction? reaction1;
 
            Reaction? reaction2;
 

	
 
            if (firstBuffer)
 
            {
 
                reaction1 = profile.FindReaction(reagent0, reagent1);
 
                reaction2 = profile.FindReaction(reagent0, reagent2);                
 
            }
 
            else
 
            {
 
                 reaction1 = profile.FindReaction(reagent0, reagent2);
 
                 reaction2 = profile.FindReaction(reagent1, 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
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;
 

	
 
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();
 
        private readonly List<Reagent> _allReagentList = new();
 
        public ObservableCollection<Reagent> BufferList { get; } = new();
 

	
 
        public ObservableCollection<Reagent> FirstBufferList { get; } = new();
 
        public ObservableCollection<Reagent> LastBufferList { get; } = new();
 

	
 
        public ReactionTestViewModel()
 
        {
 
            List<string> reagentNames = ReagentService.Names;
 
            
 
            foreach (var reagent in reagentNames.Select(ReagentService.GetReagent))
 
            {
 
                if (!reagent.IsCatalyst)
 
                { 
 
                    _allPigmentList.Add(reagent);
 
                }
 
                _allReagentList.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.
 
            PlayerProfile? profile = ProfileManager.CurrentProfile;
 
            if (profile == null) return;
 
            ReactionSet reactions = profile.Reactions;
 
            BufferList.Clear();
 
            List<Reagent> bufferList = ReactionTest.IsAllCatalysts ? _allPigmentList : _allReagentList;
 
            foreach (Reagent pigment in bufferList)
 
            foreach (Reagent buffer in bufferList)
 
            {
 
                if (pigment == ReactionTest.Reagent1) continue;
 
                if (pigment == ReactionTest.Reagent2) continue;
 
                if (reactions.Find(pigment, ReactionTest.Reagent1) == null) continue;
 
                if (reactions.Find(pigment, ReactionTest.Reagent2) == null) continue;
 
                
 
                BufferList.Add(pigment);
 
                if (buffer == ReactionTest.Reagent1) continue;
 
                if (buffer == ReactionTest.Reagent2) continue;
 

	
 

	
 
                if ((reactions.Find(buffer, ReactionTest.Reagent1) != null)
 
                    && (reactions.Find(buffer, ReactionTest.Reagent2) != null))
 
                {
 
                    FirstBufferList.Add(buffer);
 
                }
 

	
 
                if ((reactions.Find(ReactionTest.Reagent1, buffer) != null)
 
                    && (reactions.Find(ReactionTest.Reagent2, buffer) != null))
 
                {
 
                    LastBufferList.Add(buffer);
 
                }
 
            }
 

	
 
            // 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.");
 
        }
 
        
 
        public void UseFirstBuffer()
 
        {
 
            ReactionTest.UseFirstBuffer = true;
 
        }
 

	
 
        public void UseLastBuffer()
 
        {
 
            ReactionTest.UseFirstBuffer = false;
 
        }
 

	
 
        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
Views/ReactionTestView.axaml
Show inline comments
 
<UserControl xmlns="https://github.com/avaloniaui"
 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 
             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:vm="clr-namespace:DesertPaintCodex.ViewModels"
 
             mc:Ignorable="d" d:DesignWidth="310" d:DesignHeight="450"
 
             x:Class="DesertPaintCodex.Views.ReactionTestView"
 
             IsVisible="{Binding Path=!ReactionTest.IsStub}"
 
             >
 

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

	
 
    <UserControl.Styles>
 
        <Style Selector="StackPanel.ReagentRow">
 
            <Setter Property="Orientation" Value="Horizontal"/>
 
            <Setter Property="Spacing" Value="10"/>
 
            <Setter Property="Height" Value="30"/>
 
        </Style>
 
        
 
        <Style Selector="StackPanel.ReagentLabel">
 
            <Setter Property="Width" Value="80"/>
 
        </Style>
 
        
 
        <Style Selector="TextBlock.ReagentLabel">
 
            <Setter Property="VerticalAlignment" Value="Center"/>
 
            <Setter Property="TextAlignment" Value="Right"/>
 
            <Setter Property="Width" Value="80"/>
 
        </Style>
 
        
 
        <Style Selector="TextBlock.ReagentName">
 
            <Setter Property="VerticalAlignment" Value="Center"/>
 
            <Setter Property="Width" Value="120"/>
 
        </Style>
 
    </UserControl.Styles>
 

	
 
    <StackPanel Spacing="20" Margin="20 0 0 0">
 
        <StackPanel DockPanel.Dock="Top" Orientation="Vertical" Spacing="5">
 
            <TextBlock Classes="BlockHeader">REAGENTS</TextBlock>
 
            <Border Classes="ThinFrame">
 
                <StackPanel Orientation="Vertical" Spacing="10">
 
                    <StackPanel Classes="ReagentRow" IsVisible="{Binding ReactionTest.Requires3Way}">
 
                        <TextBlock Classes="ReagentLabel">Buffer:</TextBlock>
 
                        <TextBlock Classes="ReagentName" Text="{Binding ReactionTest.BufferReagent.Name, FallbackValue=[Unknown]}" IsVisible="{Binding !ReactionTest.CanPickBuffer}"/>
 
                        <ComboBox Items="{Binding BufferList}"
 
                    <StackPanel Classes="ReagentRow" IsVisible="{Binding ReactionTest.ShowFirstBuffer}">
 
                        <StackPanel Orientation="Horizontal" Classes="ReagentLabel" Spacing="10">
 
                            <Button Height="24" Width="24" Margin="8 0 0 0" Command="{Binding UseLastBuffer}">
 
                                <TextBlock VerticalAlignment="Center">▼</TextBlock>
 
                            </Button>
 
                            <TextBlock Classes="ReagentName">Buffer:</TextBlock>
 
                        </StackPanel>
 
                        <TextBlock Classes="ReagentName" Text="{Binding ReactionTest.BufferReagentFirst.Name, FallbackValue=[Unknown]}" IsVisible="{Binding !ReactionTest.CanPickBuffer}"/>
 
                        <ComboBox Items="{Binding FirstBufferList}"
 
                                  IsVisible="{Binding ReactionTest.CanPickBuffer}"
 
                                  SelectedItem="{Binding ReactionTest.BufferReagent}"
 
                                  SelectedItem="{Binding ReactionTest.BufferReagentFirst}"
 
                                  PlaceholderText="Choose" Width="120" Height="30">
 
                            <ComboBox.ItemTemplate>
 
                                <DataTemplate>
 
                                    <TextBlock Text="{Binding Name}" />
 
                                </DataTemplate>
 
                            </ComboBox.ItemTemplate>
 
                        </ComboBox>
 
                        <Border Classes="ReagentSwatch"
 
                            Background="{Binding ReactionTest.BufferReagent.Color, Converter={StaticResource paintToBrush}, FallbackValue=#00000000}" />
 
                            Background="{Binding ReactionTest.BufferReagentFirst.Color, Converter={StaticResource paintToBrush}, FallbackValue=#00000000}" />
 
                    </StackPanel>
 
                    <StackPanel Classes="ReagentRow">
 
                        <TextBlock Classes="ReagentLabel">Reagent #1:</TextBlock>
 
                        <TextBlock Classes="ReagentName" Text="{Binding ReactionTest.Reagent1.Name}" />
 
                        <Border Classes="ReagentSwatch"
 
                            Background="{Binding ReactionTest.Reagent1.Color, Converter={StaticResource paintToBrush}, FallbackValue=#00000000}" />
 
                    </StackPanel>
 
                    <StackPanel Classes="ReagentRow">
 
                        <TextBlock Classes="ReagentLabel">Reagent #2:</TextBlock>
 
                        <TextBlock Classes="ReagentName" Text="{Binding ReactionTest.Reagent2.Name}" />
 
                        <Border Classes="ReagentSwatch"
 
                            Background="{Binding ReactionTest.Reagent2.Color, Converter={StaticResource paintToBrush}, FallbackValue=#00000000}" />
 
                    </StackPanel>
 
                    <StackPanel Classes="ReagentRow" IsVisible="{Binding ReactionTest.ShowLastBuffer}">
 
                        <StackPanel Orientation="Horizontal" Classes="ReagentLabel" Spacing="10">
 
                            <Button Height="24" Width="24" Margin="8 0 0 0" Command="{Binding UseFirstBuffer}">
 
                                <TextBlock VerticalAlignment="Center">▲</TextBlock>
 
                            </Button>
 
                            <TextBlock Classes="ReagentName">Buffer:</TextBlock>
 
                        </StackPanel>
 
                        <TextBlock Classes="ReagentName" Text="{Binding ReactionTest.BufferReagentLast.Name, FallbackValue=[Unknown]}" IsVisible="{Binding !ReactionTest.CanPickBuffer}"/>
 
                        <ComboBox Items="{Binding LastBufferList}"
 
                                  IsVisible="{Binding ReactionTest.CanPickBuffer}"
 
                                  SelectedItem="{Binding ReactionTest.BufferReagentLast}"
 
                                  PlaceholderText="Choose" Width="120" Height="30">
 
                            <ComboBox.ItemTemplate>
 
                                <DataTemplate>
 
                                    <TextBlock Text="{Binding Name}" />
 
                                </DataTemplate>
 
                            </ComboBox.ItemTemplate>
 
                        </ComboBox>
 
                        <Border Classes="ReagentSwatch"
 
                                Background="{Binding ReactionTest.BufferReagentLast.Color, Converter={StaticResource paintToBrush}, FallbackValue=#00000000}" />
 
                    </StackPanel>                    
 
                </StackPanel>
 
            </Border>
 
        </StackPanel>
 

	
 
        <StackPanel DockPanel.Dock="Top" Orientation="Vertical" Spacing="5">
 
            <TextBlock Classes="BlockHeader">HYPOTHETICAL COLOR</TextBlock>
 
            <views:PaintSwatchView ShowName="False" Color="{Binding ReactionTest.HypotheticalColor}"/>
 
        </StackPanel>
 

	
 
         <Grid ColumnDefinitions="*,15,*" IsVisible="{Binding ReactionTest.CanScan}">
 
            <Button Grid.Column="0" Padding="11 5 11 5" Command="{Binding Analyze}">Analyze Mixture</Button>
 
            <Button Grid.Column="2" Padding="11 5 11 5" Command="{Binding MarkInert}">Mark Inert</Button>
 
        </Grid>
 

	
 
        <StackPanel Orientation="Vertical" Spacing="10" IsVisible="{Binding ReactionTest.IsScanning}">
 
            <ProgressBar DockPanel.Dock="Top" Value="{Binding ReactionTest.ScanProgress}" Height="20" />
 
            <Button Command="{Binding CancelScan}">Cancel Scan</Button>
 
        </StackPanel>
 

	
 
        <StackPanel DockPanel.Dock="Top" Orientation="Vertical" Spacing="5"
 
                    IsVisible="{Binding ReactionTest.HasResults}">
 
            <TextBlock Classes="BlockHeader">TEST RESULT</TextBlock>
 
            <views:PaintSwatchView ShowName="False" IsVisible="{Binding !ReactionTest.NoLab}" Color="{Binding ReactionTest.ObservedColor}"/>
 
            <views:EmbeddedWarningBox IsVisible="{Binding ReactionTest.NoLab}"
 
                Title="🛇 PIGMENT LAB NOT FOUND"
 
                Message="You may want to make sure your view of the Pigment Lab is clear, and that your screen settings are correct.">
 
                <Button Command="{Binding OpenScreenSettings}">
 
                    <TextBlock FontWeight="Bold" FontSize="15">SCREEN SETTINGS</TextBlock>
 
                </Button>
 
            </views:EmbeddedWarningBox>
 
        </StackPanel>
 

	
 
        <StackPanel DockPanel.Dock="Top" Orientation="Vertical" Spacing="5"
 
                    IsVisible="{Binding ReactionTest.HasReaction}">
 
            <TextBlock Classes="BlockHeader">REACTION OBSERVED</TextBlock>
 
            <StackPanel Orientation="Horizontal" Spacing="21" Margin="0 5">
 
                <views:ReactionUnitView Reaction="{Binding ReactionTest.Reaction}"/>
 
            </StackPanel>
 

	
 
            <views:EmbeddedWarningBox Title="🛇 REACTION CLIPPED"
 
                                      IsVisible="{Binding !!ReactionTest.Clipped}"
 
                                      Message="Your reaction fell outside of measurable values. We will need to use a buffer pigment to test it.">
 
                <StackPanel Orientation="Vertical" Spacing="10">
 
                    <Button Command="{Binding ShowClipInfo}">LEARN MORE</Button>
 
                </StackPanel>
 
            </views:EmbeddedWarningBox>
 
        </StackPanel>
 
        
 
        <Button IsVisible="{Binding ReactionTest.CanClear}"
 
                Command="{Binding ClearReaction}">Clear Reaction</Button>
 
        <Button IsVisible="{Binding ReactionTest.CanSave}"
 
                Command="{Binding SaveReaction}">Save Reaction</Button>
 
    </StackPanel>
 
</UserControl>
...
 
\ No newline at end of file
0 comments (0 inline, 0 general)