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/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 +++ 6 files changed, 238 insertions(+), 34 deletions(-) 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/StateTracking') 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 From fd77ae93d59222d70c86aebfc044f3af11063372 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Aug 2019 01:18:05 -0400 Subject: fix typos and inconsistent spelling --- .../Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs index 6550f950..32ec8c7e 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { this.AssertNotDisposed(); - // optimise for zero items + // optimize for zero items if (this.CurrentValues.Count == 0) { if (this.LastValues.Count > 0) -- cgit From 2d6175fcd27d83f7f09e570252f9e45c44115b0a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 4 Sep 2019 18:57:07 -0400 Subject: add immutable collection watcher --- .../FieldWatchers/ImmutableCollectionWatcher.cs | 37 ++++++++++++++++++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 35 +++++++++++++------- .../Framework/StateTracking/LocationTracker.cs | 5 +-- 3 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs new file mode 100644 index 00000000..30e6274f --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A collection watcher which never changes. + /// The value type within the collection. + internal class ImmutableCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + { + /********* + ** Accessors + *********/ + /// A singleton collection watcher instance. + public static ImmutableCollectionWatcher Instance { get; } = new ImmutableCollectionWatcher(); + + /// Whether the collection changed since the last reset. + public bool IsChanged { get; } = false; + + /// The values added since the last reset. + public IEnumerable Added { get; } = new TValue[0]; + + /// The values removed since the last reset. + public IEnumerable Removed { get; } = new TValue[0]; + + + /********* + ** Public methods + *********/ + /// Update the current value if needed. + public void Update() { } + + /// Set the current value as the baseline. + public void Reset() { } + + /// Stop watching the field and release all references. + public override void Dispose() { } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index 8301351e..314ff7f5 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -12,10 +12,13 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /********* ** Public methods *********/ + /**** + ** Values + ****/ /// Get a watcher which compares values using their method. This method should only be used when won't work, since this doesn't validate whether they're comparable. /// The value type. /// Get the current value. - public static ComparableWatcher ForGenericEquality(Func getValue) where T : struct + public static IValueWatcher ForGenericEquality(Func getValue) where T : struct { return new ComparableWatcher(getValue, new GenericEqualsComparer()); } @@ -23,7 +26,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// Get a watcher for an value. /// The value type. /// Get the current value. - public static ComparableWatcher ForEquatable(Func getValue) where T : IEquatable + public static IValueWatcher ForEquatable(Func getValue) where T : IEquatable { return new ComparableWatcher(getValue, new EquatableComparer()); } @@ -31,15 +34,27 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// Get a watcher which detects when an object reference changes. /// The value type. /// Get the current value. - public static ComparableWatcher ForReference(Func getValue) + public static IValueWatcher ForReference(Func getValue) { return new ComparableWatcher(getValue, new ObjectReferenceComparer()); } + /// Get a watcher for a net collection. + /// The value type. + /// The net field instance type. + /// The net collection. + public static IValueWatcher ForNetValue(NetFieldBase field) where TSelf : NetFieldBase + { + return new NetValueWatcher(field); + } + + /**** + ** Collections + ****/ /// Get a watcher which detects when an object reference in a collection changes. /// The value type. /// The observable collection. - public static ComparableListWatcher ForReferenceList(ICollection collection) + public static ICollectionWatcher ForReferenceList(ICollection collection) { return new ComparableListWatcher(collection, new ObjectReferenceComparer()); } @@ -47,24 +62,22 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// Get a watcher for an observable collection. /// The value type. /// The observable collection. - public static ObservableCollectionWatcher ForObservableCollection(ObservableCollection collection) + public static ICollectionWatcher ForObservableCollection(ObservableCollection collection) { return new ObservableCollectionWatcher(collection); } - /// Get a watcher for a net collection. + /// Get a watcher for a collection that never changes. /// The value type. - /// The net field instance type. - /// The net collection. - public static NetValueWatcher ForNetValue(NetFieldBase field) where TSelf : NetFieldBase + public static ICollectionWatcher ForImmutableCollection() { - return new NetValueWatcher(field); + return ImmutableCollectionWatcher.Instance; } /// Get a watcher for a net collection. /// The value type. /// The net collection. - public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : class, INetObject + public static ICollectionWatcher ForNetCollection(NetCollection collection) where T : class, INetObject { return new NetCollectionWatcher(collection); } diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 2249e41b..1f479e12 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; @@ -59,9 +58,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.BuildingsWatcher = location is BuildableGameLocation buildableLocation - ? WatcherFactory.ForNetCollection(buildableLocation.buildings) - : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection(buildableLocation.buildings) : WatcherFactory.ForImmutableCollection(); this.DebrisWatcher = WatcherFactory.ForNetCollection(location.debris); this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures); this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); -- cgit