diff options
-rw-r--r-- | docs/release-notes.md | 1 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 240 | ||||
-rw-r--r-- | src/SMAPI/Framework/SnapshotDiff.cs | 43 | ||||
-rw-r--r-- | src/SMAPI/Framework/SnapshotListDiff.cs | 58 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 35 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs | 59 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs | 53 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs | 66 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs | 52 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs | 7 |
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() { |