diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs
index 6f716746..aaf089d3 100644
--- a/src/SMAPI/Framework/CursorPosition.cs
+++ b/src/SMAPI/Framework/CursorPosition.cs
@@ -36,5 +36,12 @@ namespace StardewModdingAPI.Framework
this.Tile = tile;
this.GrabTile = grabTile;
}
+
+ /// Get whether the current object is equal to another object of the same type.
+ /// An object to compare with this object.
+ public bool Equals(ICursorPosition other)
+ {
+ return other != null && this.ScreenPixels == other.ScreenPixels;
+ }
}
}
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index a4d149f3..588d30c8 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -14,7 +14,6 @@ using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.StateTracking;
-using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
using StardewValley.BellsAndWhistles;
@@ -85,38 +84,8 @@ namespace StardewModdingAPI.Framework
/****
** Game state
****/
- /// The underlying watchers for convenience. These are accessible individually as separate properties.
- private readonly List Watchers = new List();
-
- /// Tracks changes to the window size.
- private IValueWatcher WindowSizeWatcher;
-
- /// Tracks changes to the current player.
- private PlayerTracker CurrentPlayerTracker;
-
- /// Tracks changes to the time of day (in 24-hour military format).
- private IValueWatcher TimeWatcher;
-
- /// Tracks changes to the save ID.
- private IValueWatcher SaveIdWatcher;
-
- /// Tracks changes to the game's locations.
- private WorldLocationsTracker LocationsWatcher;
-
- /// Tracks changes to .
- private IValueWatcher ActiveMenuWatcher;
-
- /// Tracks changes to the cursor position.
- private IValueWatcher CursorWatcher;
-
- /// Tracks changes to the mouse wheel scroll.
- private IValueWatcher MouseWheelScrollWatcher;
-
- /// The previous content locale.
- private LocalizedContentManager.LanguageCode? PreviousLocale;
-
- /// The previous cursor position.
- private ICursorPosition PreviousCursorPosition;
+ /// Monitors the entire game state for changes.
+ private WatcherCore Watchers;
/// An index incremented on every tick and reset every 60th tick (0–59).
private int CurrentUpdateTick;
@@ -186,23 +155,7 @@ namespace StardewModdingAPI.Framework
this.Input.TrueUpdate();
// init watchers
- this.CursorWatcher = WatcherFactory.ForEquatable(() => this.Input.CursorPosition.ScreenPixels);
- this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => this.Input.RealMouse.ScrollWheelValue);
- this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0);
- 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)Game1.locations);
- this.Watchers.AddRange(new IWatcher[]
- {
- this.CursorWatcher,
- this.MouseWheelScrollWatcher,
- this.SaveIdWatcher,
- this.WindowSizeWatcher,
- this.TimeWatcher,
- this.ActiveMenuWatcher,
- this.LocationsWatcher
- });
+ this.Watchers = new WatcherCore(this.Input);
// raise callback
this.OnGameInitialised();
@@ -372,44 +325,20 @@ namespace StardewModdingAPI.Framework
/*********
** Update watchers
*********/
- // reset player
- if (Context.IsWorldReady)
- {
- if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player)
- {
- this.CurrentPlayerTracker?.Dispose();
- this.CurrentPlayerTracker = new PlayerTracker(Game1.player);
- }
- }
- else
- {
- if (this.CurrentPlayerTracker != null)
- {
- this.CurrentPlayerTracker.Dispose();
- this.CurrentPlayerTracker = null;
- }
- }
-
- // update values
- foreach (IWatcher watcher in this.Watchers)
- watcher.Update();
- this.CurrentPlayerTracker?.Update();
- this.LocationsWatcher.Update();
+ this.Watchers.Update();
/*********
** Locale changed events
*********/
- if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode)
+ if (this.Watchers.LocaleWatcher.IsChanged)
{
- var oldValue = this.PreviousLocale;
- var newValue = LocalizedContentManager.CurrentLanguageCode;
+ var was = this.Watchers.LocaleWatcher.PreviousValue;
+ var now = this.Watchers.LocaleWatcher.CurrentValue;
- this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace);
+ this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace);
+ this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(was.ToString(), now.ToString()));
- if (oldValue != null)
- this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged(oldValue.ToString(), newValue.ToString()));
-
- this.PreviousLocale = newValue;
+ this.Watchers.LocaleWatcher.Reset();
}
/*********
@@ -450,12 +379,12 @@ namespace StardewModdingAPI.Framework
// event because we need to notify mods after the game handles the resize, so the
// game's metadata (like Game1.viewport) are updated. That's a bit complicated
// since the game adds & removes its own handler on the fly.
- if (this.WindowSizeWatcher.IsChanged)
+ if (this.Watchers.WindowSizeWatcher.IsChanged)
{
if (this.VerboseLogging)
- this.Monitor.Log($"Context: window size changed to {this.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace);
+ this.Monitor.Log($"Context: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace);
this.Events.Graphics_Resize.Raise();
- this.WindowSizeWatcher.Reset();
+ this.Watchers.WindowSizeWatcher.Reset();
}
/*********
@@ -470,21 +399,23 @@ namespace StardewModdingAPI.Framework
ICursorPosition cursor = this.Input.CursorPosition;
// raise cursor moved event
- if (this.CursorWatcher.IsChanged && this.PreviousCursorPosition != null)
+ if (this.Watchers.CursorWatcher.IsChanged)
{
- this.CursorWatcher.Reset();
- this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(this.PreviousCursorPosition, cursor));
+ ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue;
+ ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue;
+ this.Watchers.CursorWatcher.Reset();
+
+ this.Events.Input_CursorMoved.Raise(new InputCursorMovedArgsInput(was, now));
}
- this.PreviousCursorPosition = cursor;
// raise mouse wheel scrolled
- if (this.MouseWheelScrollWatcher.IsChanged)
+ if (this.Watchers.MouseWheelScrollWatcher.IsChanged)
{
- int oldValue = this.MouseWheelScrollWatcher.PreviousValue;
- int newValue = this.MouseWheelScrollWatcher.CurrentValue;
- this.MouseWheelScrollWatcher.Reset();
+ int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue;
+ int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue;
+ this.Watchers.MouseWheelScrollWatcher.Reset();
- this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, oldValue, newValue));
+ this.Events.Input_MouseWheelScrolled.Raise(new InputMouseWheelScrolledEventArgs(cursor, was, now));
}
// raise input button events
@@ -544,20 +475,20 @@ namespace StardewModdingAPI.Framework
/*********
** Menu events
*********/
- if (this.ActiveMenuWatcher.IsChanged)
+ if (this.Watchers.ActiveMenuWatcher.IsChanged)
{
- IClickableMenu previousMenu = this.ActiveMenuWatcher.PreviousValue;
- IClickableMenu newMenu = this.ActiveMenuWatcher.CurrentValue;
- this.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards
+ IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue;
+ IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue;
+ this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards
if (this.VerboseLogging)
- this.Monitor.Log($"Context: menu changed from {previousMenu?.GetType().FullName ?? "none"} to {newMenu?.GetType().FullName ?? "none"}.", LogLevel.Trace);
+ this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace);
// raise menu events
- if (newMenu != null)
- this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu));
+ if (now != null)
+ this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(was, now));
else
- this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu));
+ this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(was));
}
/*********
@@ -565,22 +496,22 @@ namespace StardewModdingAPI.Framework
*********/
if (Context.IsWorldReady)
{
- bool raiseWorldEvents = !this.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded
+ bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded
// raise location changes
- if (this.LocationsWatcher.IsChanged)
+ if (this.Watchers.LocationsWatcher.IsChanged)
{
// location list changes
- if (this.LocationsWatcher.IsLocationListChanged)
+ if (this.Watchers.LocationsWatcher.IsLocationListChanged)
{
- GameLocation[] added = this.LocationsWatcher.Added.ToArray();
- GameLocation[] removed = this.LocationsWatcher.Removed.ToArray();
- this.LocationsWatcher.ResetLocationList();
+ GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray();
+ GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray();
+ this.Watchers.LocationsWatcher.ResetLocationList();
if (this.VerboseLogging)
{
- string addedText = this.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
- string removedText = this.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
+ string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
+ string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace);
}
@@ -591,7 +522,7 @@ namespace StardewModdingAPI.Framework
// raise location contents changed
if (raiseWorldEvents)
{
- foreach (LocationTracker watcher in this.LocationsWatcher.Locations)
+ foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations)
{
// buildings changed
if (watcher.BuildingsWatcher.IsChanged)
@@ -652,15 +583,15 @@ namespace StardewModdingAPI.Framework
}
}
else
- this.LocationsWatcher.Reset();
+ this.Watchers.LocationsWatcher.Reset();
}
// raise time changed
- if (raiseWorldEvents && this.TimeWatcher.IsChanged)
+ if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged)
{
- int was = this.TimeWatcher.PreviousValue;
- int now = this.TimeWatcher.CurrentValue;
- this.TimeWatcher.Reset();
+ int was = this.Watchers.TimeWatcher.PreviousValue;
+ int now = this.Watchers.TimeWatcher.CurrentValue;
+ this.Watchers.TimeWatcher.Reset();
if (this.VerboseLogging)
this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace);
@@ -668,12 +599,12 @@ namespace StardewModdingAPI.Framework
this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now));
}
else
- this.TimeWatcher.Reset();
+ this.Watchers.TimeWatcher.Reset();
// raise player events
if (raiseWorldEvents)
{
- PlayerTracker curPlayer = this.CurrentPlayerTracker;
+ PlayerTracker curPlayer = this.Watchers.CurrentPlayerTracker;
// raise current location changed
if (curPlayer.TryGetNewLocation(out GameLocation newLocation))
@@ -708,11 +639,11 @@ namespace StardewModdingAPI.Framework
this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel));
}
}
- this.CurrentPlayerTracker?.Reset();
+ this.Watchers.CurrentPlayerTracker?.Reset();
}
// update save ID watcher
- this.SaveIdWatcher.Reset();
+ this.Watchers.SaveIdWatcher.Reset();
/*********
** Game update
diff --git a/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs
new file mode 100644
index 00000000..cc1d6553
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace StardewModdingAPI.Framework.StateTracking.Comparers
+{
+ /// Compares values using their method. This should only be used when won't work, since this doesn't validate whether they're comparable.
+ /// The value type.
+ internal class GenericEqualsComparer : IEqualityComparer
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Determines whether the specified objects are equal.
+ /// true if the specified objects are equal; otherwise, false.
+ /// The first object to compare.
+ /// The second object to compare.
+ public bool Equals(T x, T y)
+ {
+ if (x == null)
+ return y == null;
+ return x.Equals(y);
+ }
+
+ /// Get a hash code for the specified object.
+ /// The value.
+ public int GetHashCode(T obj)
+ {
+ return RuntimeHelpers.GetHashCode(obj);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
index 4f1ac9f4..d7a02668 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
@@ -12,6 +12,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/*********
** Public methods
*********/
+ /// Get a watcher which compares values using their method. This method should only be used when won't work, since this doesn't validate whether they're comparable.
+ /// The value type.
+ /// Get the current value.
+ public static ComparableWatcher ForGenericEquality(Func getValue) where T : struct
+ {
+ return new ComparableWatcher(getValue, new GenericEqualsComparer());
+ }
+
/// Get a watcher for an value.
/// The value type.
/// Get the current value.
diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs
new file mode 100644
index 00000000..64b063cf
--- /dev/null
+++ b/src/SMAPI/Framework/WatcherCore.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI.Framework.Input;
+using StardewModdingAPI.Framework.StateTracking;
+using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
+using StardewValley;
+using StardewValley.Menus;
+
+namespace StardewModdingAPI.Framework
+{
+ /// Monitors the entire game state for changes, virally spreading watchers into any new entities that get created.
+ internal class WatcherCore
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// The underlying watchers for convenience. These are accessible individually as separate properties.
+ private readonly List Watchers = new List();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// Tracks changes to the window size.
+ public readonly IValueWatcher WindowSizeWatcher;
+
+ /// Tracks changes to the current player.
+ public PlayerTracker CurrentPlayerTracker;
+
+ /// Tracks changes to the time of day (in 24-hour military format).
+ public readonly IValueWatcher TimeWatcher;
+
+ /// Tracks changes to the save ID.
+ public readonly IValueWatcher SaveIdWatcher;
+
+ /// Tracks changes to the game's locations.
+ public readonly WorldLocationsTracker LocationsWatcher;
+
+ /// Tracks changes to .
+ public readonly IValueWatcher ActiveMenuWatcher;
+
+ /// Tracks changes to the cursor position.
+ public readonly IValueWatcher CursorWatcher;
+
+ /// Tracks changes to the mouse wheel scroll.
+ public readonly IValueWatcher MouseWheelScrollWatcher;
+
+ /// Tracks changes to the content locale.
+ public readonly IValueWatcher LocaleWatcher;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// Manages input visible to the game.
+ public WatcherCore(SInputState inputState)
+ {
+ // init watchers
+ this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition);
+ this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.RealMouse.ScrollWheelValue);
+ this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0);
+ 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)Game1.locations);
+ this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode);
+ this.Watchers.AddRange(new IWatcher[]
+ {
+ this.CursorWatcher,
+ this.MouseWheelScrollWatcher,
+ this.SaveIdWatcher,
+ this.WindowSizeWatcher,
+ this.TimeWatcher,
+ this.ActiveMenuWatcher,
+ this.LocationsWatcher,
+ this.LocaleWatcher
+ });
+ }
+
+ /// Update the watchers and adjust for added or removed entities.
+ public void Update()
+ {
+ // reset player
+ if (Context.IsWorldReady)
+ {
+ if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player)
+ {
+ this.CurrentPlayerTracker?.Dispose();
+ this.CurrentPlayerTracker = new PlayerTracker(Game1.player);
+ }
+ }
+ else
+ {
+ if (this.CurrentPlayerTracker != null)
+ {
+ this.CurrentPlayerTracker.Dispose();
+ this.CurrentPlayerTracker = null;
+ }
+ }
+
+ // update values
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Update();
+ this.CurrentPlayerTracker?.Update();
+ this.LocationsWatcher.Update();
+ }
+
+ /// Reset the current values as the baseline.
+ public void Reset()
+ {
+ foreach (IWatcher watcher in this.Watchers)
+ watcher.Reset();
+ this.CurrentPlayerTracker?.Reset();
+ this.LocationsWatcher.Reset();
+ }
+ }
+}
diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs
index ddb8eb49..78f4fc21 100644
--- a/src/SMAPI/ICursorPosition.cs
+++ b/src/SMAPI/ICursorPosition.cs
@@ -1,9 +1,10 @@
+using System;
using Microsoft.Xna.Framework;
namespace StardewModdingAPI
{
/// Represents a cursor position in the different coordinate systems.
- public interface ICursorPosition
+ public interface ICursorPosition : IEquatable
{
/// The pixel position relative to the top-left corner of the visible screen.
Vector2 ScreenPixels { get; }
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 8e3ad83b..67c48a57 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -129,6 +129,8 @@
+
+