From d3209b17de4e46ee1d604aac24642af80ce855cc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 14 Jun 2019 01:16:50 -0400 Subject: decouple updating watchers & raising event to fix some mod changes not being tracked correctly (#648) --- src/SMAPI/Framework/SGame.cs | 240 +++++++-------------- src/SMAPI/Framework/SnapshotDiff.cs | 43 ++++ src/SMAPI/Framework/SnapshotListDiff.cs | 58 +++++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 35 +-- .../StateTracking/Snapshots/LocationSnapshot.cs | 59 +++++ .../StateTracking/Snapshots/PlayerSnapshot.cs | 53 +++++ .../StateTracking/Snapshots/WatcherSnapshot.cs | 66 ++++++ .../Snapshots/WorldLocationsSnapshot.cs | 52 +++++ .../StateTracking/WorldLocationsTracker.cs | 7 + 9 files changed, 412 insertions(+), 201 deletions(-) create mode 100644 src/SMAPI/Framework/SnapshotDiff.cs create mode 100644 src/SMAPI/Framework/SnapshotListDiff.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs (limited to 'src/SMAPI/Framework') 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 /// Monitors the entire game state for changes. private WatcherCore Watchers; + /// A snapshot of the current state. + private WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); + /// Whether post-game-startup initialisation has been performed. 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[] added = watcher.ObjectsWatcher.Added.ToArray(); - KeyValuePair[] 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[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); - KeyValuePair[] 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> 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 +{ + /// A snapshot of a tracked value. + /// The tracked value type. + internal class SnapshotDiff + { + /********* + ** Accessors + *********/ + /// Whether the value changed since the last update. + public bool IsChanged { get; private set; } + + /// The previous value. + public T Old { get; private set; } + + /// The current value. + public T New { get; private set; } + + + /********* + ** Public methods + *********/ + /// Update the snapshot. + /// Whether the value changed since the last update. + /// The previous value. + /// The current value. + public void Update(bool isChanged, T old, T now) + { + this.IsChanged = isChanged; + this.Old = old; + this.New = now; + } + + /// Update the snapshot. + /// The value watcher to snapshot. + public void Update(IValueWatcher 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 +{ + /// A snapshot of a tracked list. + /// The tracked list value type. + internal class SnapshotListDiff + { + /********* + ** Fields + *********/ + /// The removed values. + private readonly List RemovedImpl = new List(); + + /// The added values. + private readonly List AddedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last update. + public bool IsChanged { get; private set; } + + /// The removed values. + public IEnumerable Removed => this.RemovedImpl; + + /// The added values. + public IEnumerable Added => this.AddedImpl; + + + /********* + ** Public methods + *********/ + /// Update the snapshot. + /// Whether the value changed since the last update. + /// The removed values. + /// The added values. + public void Update(bool isChanged, IEnumerable removed, IEnumerable added) + { + this.IsChanged = isChanged; + + this.RemovedImpl.Clear(); + this.RemovedImpl.AddRange(removed); + + this.AddedImpl.Clear(); + this.AddedImpl.AddRange(added); + } + + /// Update the snapshot. + /// The value watcher to snapshot. + public void Update(ICollectionWatcher 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 /// The player's current location. public IValueWatcher LocationWatcher { get; } - /// The player's current mine level. - public IValueWatcher MineLevelWatcher { get; } - /// Tracks changes to the player's skill levels. public IDictionary> 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.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 } } - /// Get the player skill levels which changed. - public IEnumerable>> GetChangedSkills() - { - return this.SkillWatchers.Where(p => p.Value.IsChanged); - } - - /// Get the player's new location if it changed. - /// The player's current location. - /// Returns whether it changed. - public bool TryGetNewLocation(out GameLocation location) - { - location = this.LocationWatcher.CurrentValue; - return this.LocationWatcher.IsChanged; - } - - /// Get the player's new mine level if it changed. - /// The player's current mine level. - /// Returns whether it changed. - public bool TryGetNewMineLevel(out int mineLevel) - { - mineLevel = this.MineLevelWatcher.CurrentValue; - return this.MineLevelWatcher.IsChanged; - } - /// Stop watching the player fields and release all references. 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 +{ + /// A frozen snapshot of a tracked game location. + internal class LocationSnapshot + { + /********* + ** Accessors + *********/ + /// The tracked location. + public GameLocation Location { get; } + + /// Tracks added or removed buildings. + public SnapshotListDiff Buildings { get; } = new SnapshotListDiff(); + + /// Tracks added or removed debris. + public SnapshotListDiff Debris { get; } = new SnapshotListDiff(); + + /// Tracks added or removed large terrain features. + public SnapshotListDiff LargeTerrainFeatures { get; } = new SnapshotListDiff(); + + /// Tracks added or removed NPCs. + public SnapshotListDiff Npcs { get; } = new SnapshotListDiff(); + + /// Tracks added or removed objects. + public SnapshotListDiff> Objects { get; } = new SnapshotListDiff>(); + + /// Tracks added or removed terrain features. + public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The tracked location. + public LocationSnapshot(GameLocation location) + { + this.Location = location; + } + + /// Update the tracked values. + /// The watcher to snapshot. + 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 +{ + /// A frozen snapshot of a tracked player. + internal class PlayerSnapshot + { + /********* + ** Accessors + *********/ + /// The player being tracked. + public Farmer Player { get; } + + /// The player's current location. + public SnapshotDiff Location { get; } = new SnapshotDiff(); + + /// Tracks changes to the player's skill levels. + public IDictionary> Skills { get; } = + Enum + .GetValues(typeof(SkillType)) + .Cast() + .ToDictionary(skill => skill, skill => new SnapshotDiff()); + + /// Get a list of inventory changes. + public IEnumerable InventoryChanges { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player being tracked. + public PlayerSnapshot(Farmer player) + { + this.Player = player; + } + + /// Update the tracked values. + /// The player watcher to snapshot. + 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 +{ + /// A frozen snapshot of the game state watchers. + internal class WatcherSnapshot + { + /********* + ** Accessors + *********/ + /// Tracks changes to the window size. + public SnapshotDiff WindowSize { get; } = new SnapshotDiff(); + + /// Tracks changes to the current player. + public PlayerSnapshot CurrentPlayer { get; private set; } + + /// Tracks changes to the time of day (in 24-hour military format). + public SnapshotDiff Time { get; } = new SnapshotDiff(); + + /// Tracks changes to the save ID. + public SnapshotDiff SaveID { get; } = new SnapshotDiff(); + + /// Tracks changes to the game's locations. + public WorldLocationsSnapshot Locations { get; } = new WorldLocationsSnapshot(); + + /// Tracks changes to . + public SnapshotDiff ActiveMenu { get; } = new SnapshotDiff(); + + /// Tracks changes to the cursor position. + public SnapshotDiff Cursor { get; } = new SnapshotDiff(); + + /// Tracks changes to the mouse wheel scroll. + public SnapshotDiff MouseWheelScroll { get; } = new SnapshotDiff(); + + /// Tracks changes to the content locale. + public SnapshotDiff Locale { get; } = new SnapshotDiff(); + + + /********* + ** Public methods + *********/ + /// Update the tracked values. + /// The watchers to snapshot. + 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 +{ + /// A frozen snapshot of the tracked game locations. + internal class WorldLocationsSnapshot + { + /********* + ** Fields + *********/ + /// A map of tracked locations. + private readonly Dictionary LocationsDict = new Dictionary(new ObjectReferenceComparer()); + + + /********* + ** Accessors + *********/ + /// Tracks changes to the location list. + public SnapshotListDiff LocationList { get; } = new SnapshotListDiff(); + + /// The tracked locations. + public IEnumerable Locations => this.LocationsDict.Values; + + + /********* + ** Public methods + *********/ + /// Update the tracked values. + /// The watcher to snapshot. + 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(); } + /// Get whether the given location is tracked. + /// The location to check. + public bool HasLocationTracker(GameLocation location) + { + return this.LocationDict.ContainsKey(location); + } + /// Stop watching the player fields and release all references. public void Dispose() { -- cgit