summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-06-10 12:06:29 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-06-10 12:06:29 -0400
commit235d67623de648499db521606e4b9033d35388e5 (patch)
treebae7a46e7de77270ff09d893e0187b4bc9e9a72f /src/SMAPI
parente27ada0f6187cdfd90b2812ba61de1e2e4f4b12d (diff)
downloadSMAPI-235d67623de648499db521606e4b9033d35388e5.tar.gz
SMAPI-235d67623de648499db521606e4b9033d35388e5.tar.bz2
SMAPI-235d67623de648499db521606e4b9033d35388e5.zip
create watcher core (#310)
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/CursorPosition.cs7
-rw-r--r--src/SMAPI/Framework/SGame.cs169
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs31
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs8
-rw-r--r--src/SMAPI/Framework/WatcherCore.cs119
-rw-r--r--src/SMAPI/ICursorPosition.cs3
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj2
7 files changed, 219 insertions, 120 deletions
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;
}
+
+ /// <summary>Get whether the current object is equal to another object of the same type.</summary>
+ /// <param name="other">An object to compare with this object.</param>
+ 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
****/
- /// <summary>The underlying watchers for convenience. These are accessible individually as separate properties.</summary>
- private readonly List<IWatcher> Watchers = new List<IWatcher>();
-
- /// <summary>Tracks changes to the window size.</summary>
- private IValueWatcher<Point> WindowSizeWatcher;
-
- /// <summary>Tracks changes to the current player.</summary>
- private PlayerTracker CurrentPlayerTracker;
-
- /// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
- private IValueWatcher<int> TimeWatcher;
-
- /// <summary>Tracks changes to the save ID.</summary>
- private IValueWatcher<ulong> SaveIdWatcher;
-
- /// <summary>Tracks changes to the game's locations.</summary>
- private WorldLocationsTracker LocationsWatcher;
-
- /// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
- private IValueWatcher<IClickableMenu> ActiveMenuWatcher;
-
- /// <summary>Tracks changes to the cursor position.</summary>
- private IValueWatcher<Vector2> CursorWatcher;
-
- /// <summary>Tracks changes to the mouse wheel scroll.</summary>
- private IValueWatcher<int> MouseWheelScrollWatcher;
-
- /// <summary>The previous content locale.</summary>
- private LocalizedContentManager.LanguageCode? PreviousLocale;
-
- /// <summary>The previous cursor position.</summary>
- private ICursorPosition PreviousCursorPosition;
+ /// <summary>Monitors the entire game state for changes.</summary>
+ private WatcherCore Watchers;
/// <summary>An index incremented on every tick and reset every 60th tick (0–59).</summary>
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<GameLocation>)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;
-
- this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace);
+ var was = this.Watchers.LocaleWatcher.PreviousValue;
+ var now = this.Watchers.LocaleWatcher.CurrentValue;
- if (oldValue != null)
- this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(oldValue.ToString(), newValue.ToString()));
+ this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace);
+ this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(was.ToString(), now.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
+{
+ /// <summary>Compares values using their <see cref="object.Equals(object)"/> method. This should only be used when <see cref="EquatableComparer{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ internal class GenericEqualsComparer<T> : IEqualityComparer<T>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Determines whether the specified objects are equal.</summary>
+ /// <returns>true if the specified objects are equal; otherwise, false.</returns>
+ /// <param name="x">The first object to compare.</param>
+ /// <param name="y">The second object to compare.</param>
+ public bool Equals(T x, T y)
+ {
+ if (x == null)
+ return y == null;
+ return x.Equals(y);
+ }
+
+ /// <summary>Get a hash code for the specified object.</summary>
+ /// <param name="obj">The value.</param>
+ 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
*********/
+ /// <summary>Get a watcher which compares values using their <see cref="object.Equals(object)"/> method. This method should only be used when <see cref="ForEquatable{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="getValue">Get the current value.</param>
+ public static ComparableWatcher<T> ForGenericEquality<T>(Func<T> getValue) where T : struct
+ {
+ return new ComparableWatcher<T>(getValue, new GenericEqualsComparer<T>());
+ }
+
/// <summary>Get a watcher for an <see cref="IEquatable{T}"/> value.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="getValue">Get the current value.</param>
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
+{
+ /// <summary>Monitors the entire game state for changes, virally spreading watchers into any new entities that get created.</summary>
+ internal class WatcherCore
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>The underlying watchers for convenience. These are accessible individually as separate properties.</summary>
+ private readonly List<IWatcher> Watchers = new List<IWatcher>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Tracks changes to the window size.</summary>
+ public readonly IValueWatcher<Point> WindowSizeWatcher;
+
+ /// <summary>Tracks changes to the current player.</summary>
+ public PlayerTracker CurrentPlayerTracker;
+
+ /// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
+ public readonly IValueWatcher<int> TimeWatcher;
+
+ /// <summary>Tracks changes to the save ID.</summary>
+ public readonly IValueWatcher<ulong> SaveIdWatcher;
+
+ /// <summary>Tracks changes to the game's locations.</summary>
+ public readonly WorldLocationsTracker LocationsWatcher;
+
+ /// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
+ public readonly IValueWatcher<IClickableMenu> ActiveMenuWatcher;
+
+ /// <summary>Tracks changes to the cursor position.</summary>
+ public readonly IValueWatcher<ICursorPosition> CursorWatcher;
+
+ /// <summary>Tracks changes to the mouse wheel scroll.</summary>
+ public readonly IValueWatcher<int> MouseWheelScrollWatcher;
+
+ /// <summary>Tracks changes to the content locale.</summary>
+ public readonly IValueWatcher<LocalizedContentManager.LanguageCode> LocaleWatcher;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="inputState">Manages input visible to the game.</param>
+ 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<GameLocation>)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
+ });
+ }
+
+ /// <summary>Update the watchers and adjust for added or removed entities.</summary>
+ 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();
+ }
+
+ /// <summary>Reset the current values as the baseline.</summary>
+ 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
{
/// <summary>Represents a cursor position in the different coordinate systems.</summary>
- public interface ICursorPosition
+ public interface ICursorPosition : IEquatable<ICursorPosition>
{
/// <summary>The pixel position relative to the top-left corner of the visible screen.</summary>
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 @@
<Compile Include="Framework\Models\ManifestDependency.cs" />
<Compile Include="Framework\ModHelpers\InputHelper.cs" />
<Compile Include="Framework\Serialisation\SemanticVersionConverter.cs" />
+ <Compile Include="Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
+ <Compile Include="Framework\WatcherCore.cs" />
<Compile Include="IInputHelper.cs" />
<Compile Include="Framework\Input\SInputState.cs" />
<Compile Include="Framework\Input\InputStatus.cs" />