From dad67e213e68eb85c534d7c1c4035dfde90ff822 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com>
Date: Tue, 4 Dec 2018 23:16:13 -0500
Subject: fix world events in the mines (#603)

---
 .../FieldWatchers/ComparableListWatcher.cs         | 82 ++++++++++++++++++++++
 .../StateTracking/FieldWatchers/WatcherFactory.cs  |  8 +++
 .../StateTracking/WorldLocationsTracker.cs         | 50 +++++++++----
 src/SMAPI/Framework/WatcherCore.cs                 |  3 +-
 src/SMAPI/StardewModdingAPI.csproj                 |  1 +
 5 files changed, 128 insertions(+), 16 deletions(-)
 create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs

(limited to 'src')

diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
new file mode 100644
index 00000000..95e9ef16
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
+{
+    /// <summary>A watcher which detects changes to a collection of values using a specified <see cref="IEqualityComparer{T}"/> instance.</summary>
+    internal class ComparableListWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
+    {
+        /*********
+        ** Properties
+        *********/
+        /// <summary>The collection to watch.</summary>
+        private readonly ICollection<TValue> CurrentValues;
+
+        /// <summary>The values during the previous update.</summary>
+        private HashSet<TValue> LastValues;
+
+        /// <summary>The pairs added since the last reset.</summary>
+        private readonly List<TValue> AddedImpl = new List<TValue>();
+
+        /// <summary>The pairs demoved since the last reset.</summary>
+        private readonly List<TValue> RemovedImpl = new List<TValue>();
+
+
+        /*********
+        ** Accessors
+        *********/
+        /// <summary>Whether the value changed since the last reset.</summary>
+        public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0;
+
+        /// <summary>The values added since the last reset.</summary>
+        public IEnumerable<TValue> Added => this.AddedImpl;
+
+        /// <summary>The values removed since the last reset.</summary>
+        public IEnumerable<TValue> Removed => this.RemovedImpl;
+
+
+        /*********
+        ** Public methods
+        *********/
+        /// <summary>Construct an instance.</summary>
+        /// <param name="values">The collection to watch.</param>
+        /// <param name="comparer">The equality comparer which indicates whether two values are the same.</param>
+        public ComparableListWatcher(ICollection<TValue> values, IEqualityComparer<TValue> comparer)
+        {
+            this.CurrentValues = values;
+            this.LastValues = new HashSet<TValue>(comparer);
+        }
+
+        /// <summary>Update the current value if needed.</summary>
+        public void Update()
+        {
+            this.AssertNotDisposed();
+
+            // optimise for zero items
+            if (this.CurrentValues.Count == 0)
+            {
+                if (this.LastValues.Count > 0)
+                {
+                    this.AddedImpl.AddRange(this.LastValues);
+                    this.LastValues.Clear();
+                }
+                return;
+            }
+
+            // detect changes
+            HashSet<TValue> curValues = new HashSet<TValue>(this.CurrentValues, this.LastValues.Comparer);
+            this.RemovedImpl.AddRange(from value in this.LastValues where !curValues.Contains(value) select value);
+            this.AddedImpl.AddRange(from value in curValues where !this.LastValues.Contains(value) select value);
+            this.LastValues = curValues;
+        }
+
+        /// <summary>Set the current value as the baseline.</summary>
+        public void Reset()
+        {
+            this.AssertNotDisposed();
+
+            this.AddedImpl.Clear();
+            this.RemovedImpl.Clear();
+        }
+    }
+}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
index ab4ab0d5..8301351e 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
@@ -36,6 +36,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
             return new ComparableWatcher<T>(getValue, new ObjectReferenceComparer<T>());
         }
 
+        /// <summary>Get a watcher which detects when an object reference in a collection changes.</summary>
+        /// <typeparam name="T">The value type.</typeparam>
+        /// <param name="collection">The observable collection.</param>
+        public static ComparableListWatcher<T> ForReferenceList<T>(ICollection<T> collection)
+        {
+            return new ComparableListWatcher<T>(collection, new ObjectReferenceComparer<T>());
+        }
+
         /// <summary>Get a watcher for an observable collection.</summary>
         /// <typeparam name="T">The value type.</typeparam>
         /// <param name="collection">The observable collection.</param>
diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
index 5a259663..d9d598f8 100644
--- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
@@ -1,4 +1,3 @@
-using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
@@ -19,6 +18,9 @@ namespace StardewModdingAPI.Framework.StateTracking
         /// <summary>Tracks changes to the location list.</summary>
         private readonly ICollectionWatcher<GameLocation> LocationListWatcher;
 
+        /// <summary>Tracks changes to the list of active mine locations.</summary>
+        private readonly ICollectionWatcher<MineShaft> MineLocationListWatcher;
+
         /// <summary>A lookup of the tracked locations.</summary>
         private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>(new ObjectReferenceComparer<GameLocation>());
 
@@ -50,24 +52,34 @@ namespace StardewModdingAPI.Framework.StateTracking
         *********/
         /// <summary>Construct an instance.</summary>
         /// <param name="locations">The game's list of locations.</param>
-        public WorldLocationsTracker(ObservableCollection<GameLocation> locations)
+        /// <param name="activeMineLocations">The game's list of active mine locations.</param>
+        public WorldLocationsTracker(ObservableCollection<GameLocation> locations, IList<MineShaft> activeMineLocations)
         {
             this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
+            this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations);
         }
 
         /// <summary>Update the current value if needed.</summary>
         public void Update()
         {
-            // detect location changes
+            // detect added/removed locations
+            this.LocationListWatcher.Update();
+            this.MineLocationListWatcher.Update();
             if (this.LocationListWatcher.IsChanged)
             {
                 this.Remove(this.LocationListWatcher.Removed);
                 this.Add(this.LocationListWatcher.Added);
             }
+            if (this.MineLocationListWatcher.IsChanged)
+            {
+                this.Remove(this.MineLocationListWatcher.Removed);
+                this.Add(this.MineLocationListWatcher.Added);
+            }
 
-            // detect building changes
+            // detect building changed
             foreach (LocationTracker watcher in this.Locations.ToArray())
             {
+                watcher.Update();
                 if (watcher.BuildingsWatcher.IsChanged)
                 {
                     this.Remove(watcher.BuildingsWatcher.Removed);
@@ -75,7 +87,7 @@ namespace StardewModdingAPI.Framework.StateTracking
                 }
             }
 
-            // detect building interior changed (e.g. construction completed)
+            // detect building interiors changed (e.g. construction completed)
             foreach (KeyValuePair<Building, GameLocation> pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value)))
             {
                 GameLocation oldIndoors = pair.Value;
@@ -86,10 +98,6 @@ namespace StardewModdingAPI.Framework.StateTracking
                 if (newIndoors != null)
                     this.Removed.Add(newIndoors);
             }
-
-            // update watchers
-            foreach (IWatcher watcher in this.Locations)
-                watcher.Update();
         }
 
         /// <summary>Set the current location list as the baseline.</summary>
@@ -98,21 +106,21 @@ namespace StardewModdingAPI.Framework.StateTracking
             this.Removed.Clear();
             this.Added.Clear();
             this.LocationListWatcher.Reset();
+            this.MineLocationListWatcher.Reset();
         }
 
         /// <summary>Set the current value as the baseline.</summary>
         public void Reset()
         {
             this.ResetLocationList();
-            foreach (IWatcher watcher in this.Locations)
+            foreach (IWatcher watcher in this.GetWatchers())
                 watcher.Reset();
         }
 
         /// <summary>Stop watching the player fields and release all references.</summary>
         public void Dispose()
         {
-            this.LocationListWatcher.Dispose();
-            foreach (IWatcher watcher in this.Locations)
+            foreach (IWatcher watcher in this.GetWatchers())
                 watcher.Dispose();
         }
 
@@ -180,11 +188,11 @@ namespace StardewModdingAPI.Framework.StateTracking
             // remove old location if needed
             this.Remove(location);
 
-            // track change
+            // add location
             this.Added.Add(location);
-
-            // add
             this.LocationDict[location] = new LocationTracker(location);
+
+            // add buildings
             if (location is BuildableGameLocation buildableLocation)
                 this.Add(buildableLocation.buildings);
         }
@@ -219,5 +227,17 @@ namespace StardewModdingAPI.Framework.StateTracking
                     this.Remove(buildableLocation.buildings);
             }
         }
+
+        /****
+        ** Helpers
+        ****/
+        /// <summary>The underlying watchers.</summary>
+        private IEnumerable<IWatcher> GetWatchers()
+        {
+            yield return this.LocationListWatcher;
+            yield return this.MineLocationListWatcher;
+            foreach (LocationTracker watcher in this.Locations)
+                yield return watcher;
+        }
     }
 }
diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs
index e06423b9..8d29cf18 100644
--- a/src/SMAPI/Framework/WatcherCore.cs
+++ b/src/SMAPI/Framework/WatcherCore.cs
@@ -5,6 +5,7 @@ using StardewModdingAPI.Framework.Input;
 using StardewModdingAPI.Framework.StateTracking;
 using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
 using StardewValley;
+using StardewValley.Locations;
 using StardewValley.Menus;
 
 namespace StardewModdingAPI.Framework
@@ -64,7 +65,7 @@ namespace StardewModdingAPI.Framework
             this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
             this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
             this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
-            this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations);
+            this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations, MineShaft.activeMines);
             this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode);
             this.Watchers.AddRange(new IWatcher[]
             {
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 3696b54d..49a88f37 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -205,6 +205,7 @@
     <Compile Include="Framework\SModHooks.cs" />
     <Compile Include="Framework\Singleton.cs" />
     <Compile Include="Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
+    <Compile Include="Framework\StateTracking\FieldWatchers\ComparableListWatcher.cs" />
     <Compile Include="Framework\WatcherCore.cs" />
     <Compile Include="IDataHelper.cs" />
     <Compile Include="IInputHelper.cs" />
-- 
cgit