summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md1
-rw-r--r--src/SMAPI/Framework/SGame.cs240
-rw-r--r--src/SMAPI/Framework/SnapshotDiff.cs43
-rw-r--r--src/SMAPI/Framework/SnapshotListDiff.cs58
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs35
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs59
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs53
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs66
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs52
-rw-r--r--src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs7
10 files changed, 413 insertions, 201 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index b7cd61c6..6d883baf 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -32,6 +32,7 @@ These changes have not been released yet.
* Removed all deprecated APIs.
* Removed the `Monitor.ExitGameImmediately` method.
* Updated to Json.NET 12.0.1.
+ * Fixed issue where mod changes weren't tracked correctly for raising events in some cases. Events now reflect a frozen snapshot of the game state, and any mod changes are reflected in the next event tick.
* Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save.
## 2.11.3
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 1145207f..7222899a 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -16,19 +16,16 @@ using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Reflection;
-using StardewModdingAPI.Framework.StateTracking;
+using StardewModdingAPI.Framework.StateTracking.Snapshots;
using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewValley;
using StardewValley.BellsAndWhistles;
-using StardewValley.Buildings;
using StardewValley.Events;
using StardewValley.Locations;
using StardewValley.Menus;
-using StardewValley.TerrainFeatures;
using StardewValley.Tools;
using xTile.Dimensions;
-using SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework
{
@@ -93,6 +90,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Monitors the entire game state for changes.</summary>
private WatcherCore Watchers;
+ /// <summary>A snapshot of the current <see cref="Watchers"/> state.</summary>
+ private WatcherSnapshot WatcherSnapshot = new WatcherSnapshot();
+
/// <summary>Whether post-game-startup initialisation has been performed.</summary>
private bool IsInitialised;
@@ -443,8 +443,12 @@ namespace StardewModdingAPI.Framework
/*********
** Update watchers
+ ** (Watchers need to be updated, checked, and reset in one go so we can detect any changes mods make in event handlers.)
*********/
this.Watchers.Update();
+ this.WatcherSnapshot.Update(this.Watchers);
+ this.Watchers.Reset();
+ WatcherSnapshot state = this.WatcherSnapshot;
/*********
** Pre-update events
@@ -473,12 +477,8 @@ namespace StardewModdingAPI.Framework
/*********
** Locale changed events
*********/
- if (this.Watchers.LocaleWatcher.IsChanged)
- {
- this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace);
-
- this.Watchers.LocaleWatcher.Reset();
- }
+ if (state.Locale.IsChanged)
+ this.Monitor.Log($"Context: locale set to {state.Locale.New}.", LogLevel.Trace);
/*********
** Load / return-to-title events
@@ -511,16 +511,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.Watchers.WindowSizeWatcher.IsChanged)
+ if (state.WindowSize.IsChanged)
{
if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace);
+ this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}.", LogLevel.Trace);
- Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue;
- Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue;
-
- events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize));
- this.Watchers.WindowSizeWatcher.Reset();
+ events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New));
}
/*********
@@ -535,35 +531,15 @@ namespace StardewModdingAPI.Framework
ICursorPosition cursor = this.Input.CursorPosition;
// raise cursor moved event
- if (this.Watchers.CursorWatcher.IsChanged)
- {
- if (events.CursorMoved.HasListeners())
- {
- ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue;
- ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue;
- this.Watchers.CursorWatcher.Reset();
-
- events.CursorMoved.Raise(new CursorMovedEventArgs(was, now));
- }
- else
- this.Watchers.CursorWatcher.Reset();
- }
+ if (state.Cursor.IsChanged)
+ events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old, state.Cursor.New));
// raise mouse wheel scrolled
- if (this.Watchers.MouseWheelScrollWatcher.IsChanged)
+ if (state.MouseWheelScroll.IsChanged)
{
- if (events.MouseWheelScrolled.HasListeners() || this.Monitor.IsVerbose)
- {
- int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue;
- int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue;
- this.Watchers.MouseWheelScrollWatcher.Reset();
-
- if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace);
- events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now));
- }
- else
- this.Watchers.MouseWheelScrollWatcher.Reset();
+ if (this.Monitor.IsVerbose)
+ this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}.", LogLevel.Trace);
+ events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New));
}
// raise input button events
@@ -593,17 +569,13 @@ namespace StardewModdingAPI.Framework
/*********
** Menu events
*********/
- if (this.Watchers.ActiveMenuWatcher.IsChanged)
+ if (state.ActiveMenu.IsChanged)
{
- 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.Monitor.IsVerbose)
- this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace);
+ this.Monitor.Log($"Context: menu changed from {state.ActiveMenu.Old?.GetType().FullName ?? "none"} to {state.ActiveMenu.New?.GetType().FullName ?? "none"}.", LogLevel.Trace);
// raise menu events
- events.MenuChanged.Raise(new MenuChangedEventArgs(was, now));
+ events.MenuChanged.Raise(new MenuChangedEventArgs(state.ActiveMenu.Old, state.ActiveMenu.New));
}
/*********
@@ -611,163 +583,97 @@ namespace StardewModdingAPI.Framework
*********/
if (Context.IsWorldReady)
{
- bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded
+ bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded
- // raise location changes
- if (this.Watchers.LocationsWatcher.IsChanged)
+ // location list changes
+ if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || this.Monitor.IsVerbose))
{
- // location list changes
- if (this.Watchers.LocationsWatcher.IsLocationListChanged)
- {
- GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray();
- GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray();
- this.Watchers.LocationsWatcher.ResetLocationList();
-
- if (this.Monitor.IsVerbose)
- {
- string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
- string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
- this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace);
- }
+ var added = state.Locations.LocationList.Added.ToArray();
+ var removed = state.Locations.LocationList.Removed.ToArray();
- events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed));
- }
-
- // raise location contents changed
- if (raiseWorldEvents)
+ if (this.Monitor.IsVerbose)
{
- foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations)
- {
- // buildings changed
- if (watcher.BuildingsWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- Building[] added = watcher.BuildingsWatcher.Added.ToArray();
- Building[] removed = watcher.BuildingsWatcher.Removed.ToArray();
- watcher.BuildingsWatcher.Reset();
-
- events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed));
- }
-
- // debris changed
- if (watcher.DebrisWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- Debris[] added = watcher.DebrisWatcher.Added.ToArray();
- Debris[] removed = watcher.DebrisWatcher.Removed.ToArray();
- watcher.DebrisWatcher.Reset();
-
- events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, added, removed));
- }
+ string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
+ string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
+ this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace);
+ }
- // large terrain features changed
- if (watcher.LargeTerrainFeaturesWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray();
- LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray();
- watcher.LargeTerrainFeaturesWatcher.Reset();
+ events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed));
+ }
- events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, added, removed));
- }
+ // raise location contents changed
+ if (raiseWorldEvents)
+ {
+ foreach (LocationSnapshot locState in state.Locations.Locations)
+ {
+ var location = locState.Location;
- // NPCs changed
- if (watcher.NpcsWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- NPC[] added = watcher.NpcsWatcher.Added.ToArray();
- NPC[] removed = watcher.NpcsWatcher.Removed.ToArray();
- watcher.NpcsWatcher.Reset();
+ // buildings changed
+ if (locState.Buildings.IsChanged)
+ events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, locState.Buildings.Added, locState.Buildings.Removed));
- events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, added, removed));
- }
+ // debris changed
+ if (locState.Debris.IsChanged)
+ events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, locState.Debris.Added, locState.Debris.Removed));
- // objects changed
- if (watcher.ObjectsWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- KeyValuePair<Vector2, SObject>[] added = watcher.ObjectsWatcher.Added.ToArray();
- KeyValuePair<Vector2, SObject>[] removed = watcher.ObjectsWatcher.Removed.ToArray();
- watcher.ObjectsWatcher.Reset();
+ // large terrain features changed
+ if (locState.LargeTerrainFeatures.IsChanged)
+ events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, locState.LargeTerrainFeatures.Added, locState.LargeTerrainFeatures.Removed));
- events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed));
- }
+ // NPCs changed
+ if (locState.Npcs.IsChanged)
+ events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, locState.Npcs.Added, locState.Npcs.Removed));
- // terrain features changed
- if (watcher.TerrainFeaturesWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- KeyValuePair<Vector2, TerrainFeature>[] added = watcher.TerrainFeaturesWatcher.Added.ToArray();
- KeyValuePair<Vector2, TerrainFeature>[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray();
- watcher.TerrainFeaturesWatcher.Reset();
+ // objects changed
+ if (locState.Objects.IsChanged)
+ events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed));
- events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, added, removed));
- }
- }
+ // terrain features changed
+ if (locState.TerrainFeatures.IsChanged)
+ events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed));
}
- else
- this.Watchers.LocationsWatcher.Reset();
}
// raise time changed
- if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged)
- {
- int was = this.Watchers.TimeWatcher.PreviousValue;
- int now = this.Watchers.TimeWatcher.CurrentValue;
- this.Watchers.TimeWatcher.Reset();
-
- if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace);
-
- events.TimeChanged.Raise(new TimeChangedEventArgs(was, now));
- }
- else
- this.Watchers.TimeWatcher.Reset();
+ if (raiseWorldEvents && state.Time.IsChanged)
+ events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New));
// raise player events
if (raiseWorldEvents)
{
- PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker;
+ PlayerSnapshot playerState = state.CurrentPlayer;
+ Farmer player = playerState.Player;
// raise current location changed
- if (playerTracker.TryGetNewLocation(out GameLocation newLocation))
+ if (playerState.Location.IsChanged)
{
if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace);
+ this.Monitor.Log($"Context: set location to {playerState.Location.New}.", LogLevel.Trace);
- GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue;
- events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation));
+ events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old, playerState.Location.New));
}
// raise player leveled up a skill
- foreach (KeyValuePair<SkillType, IValueWatcher<int>> pair in playerTracker.GetChangedSkills())
+ foreach (var pair in playerState.Skills)
{
+ if (!pair.Value.IsChanged)
+ continue;
+
if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace);
+ this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.Old} to {pair.Value.New}.", LogLevel.Trace);
- events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue));
+ events.LevelChanged.Raise(new LevelChangedEventArgs(player, pair.Key, pair.Value.Old, pair.Value.New));
}
// raise player inventory changed
- ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray();
+ ItemStackChange[] changedItems = playerState.InventoryChanges.ToArray();
if (changedItems.Any())
{
if (this.Monitor.IsVerbose)
this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace);
- events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems));
- }
-
- // raise mine level changed
- if (playerTracker.TryGetNewMineLevel(out int mineLevel))
- {
- if (this.Monitor.IsVerbose)
- this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace);
+ events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, changedItems));
}
}
- this.Watchers.CurrentPlayerTracker?.Reset();
-
- // update save ID watcher
- this.Watchers.SaveIdWatcher.Reset();
}
/*********
diff --git a/src/SMAPI/Framework/SnapshotDiff.cs b/src/SMAPI/Framework/SnapshotDiff.cs
new file mode 100644
index 00000000..5b6288ff
--- /dev/null
+++ b/src/SMAPI/Framework/SnapshotDiff.cs
@@ -0,0 +1,43 @@
+using StardewModdingAPI.Framework.StateTracking;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>A snapshot of a tracked value.</summary>
+ /// <typeparam name="T">The tracked value type.</typeparam>
+ internal class SnapshotDiff<T>
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the value changed since the last update.</summary>
+ public bool IsChanged { get; private set; }
+
+ /// <summary>The previous value.</summary>
+ public T Old { get; private set; }
+
+ /// <summary>The current value.</summary>
+ public T New { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Update the snapshot.</summary>
+ /// <param name="isChanged">Whether the value changed since the last update.</param>
+ /// <param name="old">The previous value.</param>
+ /// <param name="now">The current value.</param>
+ public void Update(bool isChanged, T old, T now)
+ {
+ this.IsChanged = isChanged;
+ this.Old = old;
+ this.New = now;
+ }
+
+ /// <summary>Update the snapshot.</summary>
+ /// <param name="watcher">The value watcher to snapshot.</param>
+ public void Update(IValueWatcher<T> watcher)
+ {
+ this.Update(watcher.IsChanged, watcher.PreviousValue, watcher.CurrentValue);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs
new file mode 100644
index 00000000..d4d5df50
--- /dev/null
+++ b/src/SMAPI/Framework/SnapshotListDiff.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using StardewModdingAPI.Framework.StateTracking;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>A snapshot of a tracked list.</summary>
+ /// <typeparam name="T">The tracked list value type.</typeparam>
+ internal class SnapshotListDiff<T>
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The removed values.</summary>
+ private readonly List<T> RemovedImpl = new List<T>();
+
+ /// <summary>The added values.</summary>
+ private readonly List<T> AddedImpl = new List<T>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the value changed since the last update.</summary>
+ public bool IsChanged { get; private set; }
+
+ /// <summary>The removed values.</summary>
+ public IEnumerable<T> Removed => this.RemovedImpl;
+
+ /// <summary>The added values.</summary>
+ public IEnumerable<T> Added => this.AddedImpl;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Update the snapshot.</summary>
+ /// <param name="isChanged">Whether the value changed since the last update.</param>
+ /// <param name="removed">The removed values.</param>
+ /// <param name="added">The added values.</param>
+ public void Update(bool isChanged, IEnumerable<T> removed, IEnumerable<T> added)
+ {
+ this.IsChanged = isChanged;
+
+ this.RemovedImpl.Clear();
+ this.RemovedImpl.AddRange(removed);
+
+ this.AddedImpl.Clear();
+ this.AddedImpl.AddRange(added);
+ }
+
+ /// <summary>Update the snapshot.</summary>
+ /// <param name="watcher">The value watcher to snapshot.</param>
+ public void Update(ICollectionWatcher<T> watcher)
+ {
+ this.Update(watcher.IsChanged, watcher.Removed, watcher.Added);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
index abb4fa24..6302a889 100644
--- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
@@ -5,7 +5,6 @@ using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
-using StardewValley.Locations;
using ChangeType = StardewModdingAPI.Events.ChangeType;
namespace StardewModdingAPI.Framework.StateTracking
@@ -38,9 +37,6 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>The player's current location.</summary>
public IValueWatcher<GameLocation> LocationWatcher { get; }
- /// <summary>The player's current mine level.</summary>
- public IValueWatcher<int> MineLevelWatcher { get; }
-
/// <summary>Tracks changes to the player's skill levels.</summary>
public IDictionary<SkillType, IValueWatcher<int>> SkillWatchers { get; }
@@ -58,7 +54,6 @@ namespace StardewModdingAPI.Framework.StateTracking
// init trackers
this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation);
- this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0);
this.SkillWatchers = new Dictionary<SkillType, IValueWatcher<int>>
{
[SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel),
@@ -70,11 +65,7 @@ namespace StardewModdingAPI.Framework.StateTracking
};
// track watchers for convenience
- this.Watchers.AddRange(new IWatcher[]
- {
- this.LocationWatcher,
- this.MineLevelWatcher
- });
+ this.Watchers.Add(this.LocationWatcher);
this.Watchers.AddRange(this.SkillWatchers.Values);
}
@@ -124,30 +115,6 @@ namespace StardewModdingAPI.Framework.StateTracking
}
}
- /// <summary>Get the player skill levels which changed.</summary>
- public IEnumerable<KeyValuePair<SkillType, IValueWatcher<int>>> GetChangedSkills()
- {
- return this.SkillWatchers.Where(p => p.Value.IsChanged);
- }
-
- /// <summary>Get the player's new location if it changed.</summary>
- /// <param name="location">The player's current location.</param>
- /// <returns>Returns whether it changed.</returns>
- public bool TryGetNewLocation(out GameLocation location)
- {
- location = this.LocationWatcher.CurrentValue;
- return this.LocationWatcher.IsChanged;
- }
-
- /// <summary>Get the player's new mine level if it changed.</summary>
- /// <param name="mineLevel">The player's current mine level.</param>
- /// <returns>Returns whether it changed.</returns>
- public bool TryGetNewMineLevel(out int mineLevel)
- {
- mineLevel = this.MineLevelWatcher.CurrentValue;
- return this.MineLevelWatcher.IsChanged;
- }
-
/// <summary>Stop watching the player fields and release all references.</summary>
public void Dispose()
{
diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs
new file mode 100644
index 00000000..d3029540
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.TerrainFeatures;
+
+namespace StardewModdingAPI.Framework.StateTracking.Snapshots
+{
+ /// <summary>A frozen snapshot of a tracked game location.</summary>
+ internal class LocationSnapshot
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The tracked location.</summary>
+ public GameLocation Location { get; }
+
+ /// <summary>Tracks added or removed buildings.</summary>
+ public SnapshotListDiff<Building> Buildings { get; } = new SnapshotListDiff<Building>();
+
+ /// <summary>Tracks added or removed debris.</summary>
+ public SnapshotListDiff<Debris> Debris { get; } = new SnapshotListDiff<Debris>();
+
+ /// <summary>Tracks added or removed large terrain features.</summary>
+ public SnapshotListDiff<LargeTerrainFeature> LargeTerrainFeatures { get; } = new SnapshotListDiff<LargeTerrainFeature>();
+
+ /// <summary>Tracks added or removed NPCs.</summary>
+ public SnapshotListDiff<NPC> Npcs { get; } = new SnapshotListDiff<NPC>();
+
+ /// <summary>Tracks added or removed objects.</summary>
+ public SnapshotListDiff<KeyValuePair<Vector2, Object>> Objects { get; } = new SnapshotListDiff<KeyValuePair<Vector2, Object>>();
+
+ /// <summary>Tracks added or removed terrain features.</summary>
+ public SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>> TerrainFeatures { get; } = new SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="location">The tracked location.</param>
+ public LocationSnapshot(GameLocation location)
+ {
+ this.Location = location;
+ }
+
+ /// <summary>Update the tracked values.</summary>
+ /// <param name="watcher">The watcher to snapshot.</param>
+ public void Update(LocationTracker watcher)
+ {
+ this.Buildings.Update(watcher.BuildingsWatcher);
+ this.Debris.Update(watcher.DebrisWatcher);
+ this.LargeTerrainFeatures.Update(watcher.LargeTerrainFeaturesWatcher);
+ this.Npcs.Update(watcher.NpcsWatcher);
+ this.Objects.Update(watcher.ObjectsWatcher);
+ this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs
new file mode 100644
index 00000000..7bcd9f82
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Enums;
+using StardewModdingAPI.Events;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.StateTracking.Snapshots
+{
+ /// <summary>A frozen snapshot of a tracked player.</summary>
+ internal class PlayerSnapshot
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The player being tracked.</summary>
+ public Farmer Player { get; }
+
+ /// <summary>The player's current location.</summary>
+ public SnapshotDiff<GameLocation> Location { get; } = new SnapshotDiff<GameLocation>();
+
+ /// <summary>Tracks changes to the player's skill levels.</summary>
+ public IDictionary<SkillType, SnapshotDiff<int>> Skills { get; } =
+ Enum
+ .GetValues(typeof(SkillType))
+ .Cast<SkillType>()
+ .ToDictionary(skill => skill, skill => new SnapshotDiff<int>());
+
+ /// <summary>Get a list of inventory changes.</summary>
+ public IEnumerable<ItemStackChange> InventoryChanges { get; private set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="player">The player being tracked.</param>
+ public PlayerSnapshot(Farmer player)
+ {
+ this.Player = player;
+ }
+
+ /// <summary>Update the tracked values.</summary>
+ /// <param name="watcher">The player watcher to snapshot.</param>
+ public void Update(PlayerTracker watcher)
+ {
+ this.Location.Update(watcher.LocationWatcher);
+ foreach (var pair in this.Skills)
+ pair.Value.Update(watcher.SkillWatchers[pair.Key]);
+ this.InventoryChanges = watcher.GetInventoryChanges().ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs
new file mode 100644
index 00000000..cf51e040
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs
@@ -0,0 +1,66 @@
+using Microsoft.Xna.Framework;
+using StardewValley;
+using StardewValley.Menus;
+
+namespace StardewModdingAPI.Framework.StateTracking.Snapshots
+{
+ /// <summary>A frozen snapshot of the game state watchers.</summary>
+ internal class WatcherSnapshot
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Tracks changes to the window size.</summary>
+ public SnapshotDiff<Point> WindowSize { get; } = new SnapshotDiff<Point>();
+
+ /// <summary>Tracks changes to the current player.</summary>
+ public PlayerSnapshot CurrentPlayer { get; private set; }
+
+ /// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
+ public SnapshotDiff<int> Time { get; } = new SnapshotDiff<int>();
+
+ /// <summary>Tracks changes to the save ID.</summary>
+ public SnapshotDiff<ulong> SaveID { get; } = new SnapshotDiff<ulong>();
+
+ /// <summary>Tracks changes to the game's locations.</summary>
+ public WorldLocationsSnapshot Locations { get; } = new WorldLocationsSnapshot();
+
+ /// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
+ public SnapshotDiff<IClickableMenu> ActiveMenu { get; } = new SnapshotDiff<IClickableMenu>();
+
+ /// <summary>Tracks changes to the cursor position.</summary>
+ public SnapshotDiff<ICursorPosition> Cursor { get; } = new SnapshotDiff<ICursorPosition>();
+
+ /// <summary>Tracks changes to the mouse wheel scroll.</summary>
+ public SnapshotDiff<int> MouseWheelScroll { get; } = new SnapshotDiff<int>();
+
+ /// <summary>Tracks changes to the content locale.</summary>
+ public SnapshotDiff<LocalizedContentManager.LanguageCode> Locale { get; } = new SnapshotDiff<LocalizedContentManager.LanguageCode>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Update the tracked values.</summary>
+ /// <param name="watchers">The watchers to snapshot.</param>
+ public void Update(WatcherCore watchers)
+ {
+ // update player instance
+ if (watchers.CurrentPlayerTracker == null)
+ this.CurrentPlayer = null;
+ else if (watchers.CurrentPlayerTracker.Player != this.CurrentPlayer?.Player)
+ this.CurrentPlayer = new PlayerSnapshot(watchers.CurrentPlayerTracker.Player);
+
+ // update snapshots
+ this.WindowSize.Update(watchers.WindowSizeWatcher);
+ this.Locale.Update(watchers.LocaleWatcher);
+ this.CurrentPlayer?.Update(watchers.CurrentPlayerTracker);
+ this.Time.Update(watchers.TimeWatcher);
+ this.SaveID.Update(watchers.SaveIdWatcher);
+ this.Locations.Update(watchers.LocationsWatcher);
+ this.ActiveMenu.Update(watchers.ActiveMenuWatcher);
+ this.Cursor.Update(watchers.CursorWatcher);
+ this.MouseWheelScroll.Update(watchers.MouseWheelScrollWatcher);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs
new file mode 100644
index 00000000..73ed2d8f
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Framework.StateTracking.Comparers;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.StateTracking.Snapshots
+{
+ /// <summary>A frozen snapshot of the tracked game locations.</summary>
+ internal class WorldLocationsSnapshot
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>A map of tracked locations.</summary>
+ private readonly Dictionary<GameLocation, LocationSnapshot> LocationsDict = new Dictionary<GameLocation, LocationSnapshot>(new ObjectReferenceComparer<GameLocation>());
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Tracks changes to the location list.</summary>
+ public SnapshotListDiff<GameLocation> LocationList { get; } = new SnapshotListDiff<GameLocation>();
+
+ /// <summary>The tracked locations.</summary>
+ public IEnumerable<LocationSnapshot> Locations => this.LocationsDict.Values;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Update the tracked values.</summary>
+ /// <param name="watcher">The watcher to snapshot.</param>
+ public void Update(WorldLocationsTracker watcher)
+ {
+ // update location list
+ this.LocationList.Update(watcher.IsLocationListChanged, watcher.Added, watcher.Removed);
+
+ // remove missing locations
+ foreach (var key in this.LocationsDict.Keys.Where(key => !watcher.HasLocationTracker(key)).ToArray())
+ this.LocationsDict.Remove(key);
+
+ // update locations
+ foreach (LocationTracker locationWatcher in watcher.Locations)
+ {
+ if (!this.LocationsDict.TryGetValue(locationWatcher.Location, out LocationSnapshot snapshot))
+ this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher.Location);
+
+ snapshot.Update(locationWatcher);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
index f09c69c1..303a4f3a 100644
--- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
@@ -117,6 +117,13 @@ namespace StardewModdingAPI.Framework.StateTracking
watcher.Reset();
}
+ /// <summary>Get whether the given location is tracked.</summary>
+ /// <param name="location">The location to check.</param>
+ public bool HasLocationTracker(GameLocation location)
+ {
+ return this.LocationDict.ContainsKey(location);
+ }
+
/// <summary>Stop watching the player fields and release all references.</summary>
public void Dispose()
{