diff options
Diffstat (limited to 'src/SMAPI/Framework/StateTracking')
5 files changed, 338 insertions, 37 deletions
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs new file mode 100644 index 00000000..f92edb90 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// <summary>A watcher which detects changes to a Netcode collection.</summary> + internal class NetCollectionWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue> + where TValue : INetObject<INetSerializable> + { + /********* + ** Properties + *********/ + /// <summary>The field being watched.</summary> + private readonly NetCollection<TValue> Field; + + /// <summary>The pairs added since the last reset.</summary> + private readonly List<TValue> AddedImpl = new List<TValue>(); + + /// <summary>The pairs demoved since the last reset.</summary> + private readonly List<TValue> RemovedImpl = new List<TValue>(); + + + /********* + ** Accessors + *********/ + /// <summary>Whether the collection changed since the last reset.</summary> + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// <summary>The values added since the last reset.</summary> + public IEnumerable<TValue> Added => this.AddedImpl; + + /// <summary>The values removed since the last reset.</summary> + public IEnumerable<TValue> Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="field">The field to watch.</param> + public NetCollectionWatcher(NetCollection<TValue> field) + { + this.Field = field; + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// <summary>Update the current value if needed.</summary> + public void Update() + { + this.AssertNotDisposed(); + } + + /// <summary>Set the current value as the baseline.</summary> + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// <summary>Stop watching the field and release all references.</summary> + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>A callback invoked when an entry is added to the collection.</summary> + /// <param name="value">The added value.</param> + private void OnValueAdded(TValue value) + { + this.AddedImpl.Add(value); + } + + /// <summary>A callback invoked when an entry is removed from the collection.</summary> + /// <param name="value">The added value.</param> + private void OnValueRemoved(TValue value) + { + this.RemovedImpl.Add(value); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index bf261bb5..a4982faa 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -36,6 +36,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new ObservableCollectionWatcher<T>(collection); } + /// <summary>Get a watcher for a net collection.</summary> + /// <typeparam name="T">The value type.</typeparam> + /// <param name="collection">The net collection.</param> + public static NetCollectionWatcher<T> ForNetCollection<T>(NetCollection<T> collection) where T : INetObject<INetSerializable> + { + return new NetCollectionWatcher<T>(collection); + } + /// <summary>Get a watcher for a net dictionary.</summary> /// <typeparam name="TKey">The dictionary key type.</typeparam> /// <typeparam name="TValue">The dictionary value type.</typeparam> diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 8cf4e7a2..07570401 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; using Object = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -26,8 +29,11 @@ namespace StardewModdingAPI.Framework.StateTracking /// <summary>The tracked location.</summary> public GameLocation Location { get; } - /// <summary>Tracks changes to the current location's objects.</summary> - public IDictionaryWatcher<Vector2, Object> LocationObjectsWatcher { get; } + /// <summary>Tracks changes to the location's buildings.</summary> + public ICollectionWatcher<Building> BuildingsWatcher { get; } + + /// <summary>Tracks changes to the location's objects.</summary> + public IDictionaryWatcher<Vector2, Object> ObjectsWatcher { get; } /********* @@ -40,10 +46,15 @@ namespace StardewModdingAPI.Framework.StateTracking this.Location = location; // init watchers - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); - this.Watchers.AddRange(new[] + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.BuildingsWatcher = location is BuildableGameLocation buildableLocation + ? WatcherFactory.ForNetCollection(buildableLocation.buildings) + : (ICollectionWatcher<Building>)WatcherFactory.ForObservableCollection(new ObservableCollection<Building>()); + + this.Watchers.AddRange(new IWatcher[] { - this.LocationObjectsWatcher + this.BuildingsWatcher, + this.ObjectsWatcher }); } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 032705b7..dea2e30d 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Xna.Framework; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking { @@ -38,9 +36,6 @@ namespace StardewModdingAPI.Framework.StateTracking /// <summary>The player's current location.</summary> public IValueWatcher<GameLocation> LocationWatcher { get; } - /// <summary>Tracks changes to the player's current location's objects.</summary> - public IDictionaryWatcher<Vector2, SObject> LocationObjectsWatcher { get; private set; } - /// <summary>The player's current mine level.</summary> public IValueWatcher<int> MineLevelWatcher { get; } @@ -61,7 +56,6 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary<EventArgsLevelUp.LevelType, IValueWatcher<int>> { @@ -77,7 +71,6 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.AddRange(new IWatcher[] { this.LocationWatcher, - this.LocationObjectsWatcher, this.MineLevelWatcher }); this.Watchers.AddRange(this.SkillWatchers.Values); @@ -93,16 +86,6 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (IWatcher watcher in this.Watchers) watcher.Update(); - // replace location objects watcher - if (this.LocationWatcher.IsChanged) - { - this.Watchers.Remove(this.LocationObjectsWatcher); - this.LocationObjectsWatcher.Dispose(); - - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); - this.Watchers.Add(this.LocationObjectsWatcher); - } - // update inventory this.CurrentInventory = this.GetInventory(); } @@ -154,21 +137,6 @@ namespace StardewModdingAPI.Framework.StateTracking return this.LocationWatcher.IsChanged; } - /// <summary>Get object changes to the player's current location if they there as of the last reset.</summary> - /// <param name="watcher">The object change watcher.</param> - /// <returns>Returns whether it changed.</returns> - public bool TryGetLocationChanges(out IDictionaryWatcher<Vector2, SObject> watcher) - { - if (this.LocationWatcher.IsChanged) - { - watcher = null; - return false; - } - - watcher = this.LocationObjectsWatcher; - return watcher.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> diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs new file mode 100644 index 00000000..d9090c08 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// <summary>Detects changes to the game's locations.</summary> + internal class WorldLocationsTracker : IWatcher + { + /********* + ** Properties + *********/ + /// <summary>Tracks changes to the location list.</summary> + private readonly ICollectionWatcher<GameLocation> LocationListWatcher; + + /// <summary>A lookup of the tracked locations.</summary> + private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>(); + + /// <summary>A lookup of registered buildings and their indoor location.</summary> + private readonly IDictionary<Building, GameLocation> BuildingIndoors = new Dictionary<Building, GameLocation>(); + + + /********* + ** Accessors + *********/ + /// <summary>Whether locations were added or removed since the last reset.</summary> + public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any(); + + /// <summary>Whether any tracked location data changed since the last reset.</summary> + public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged); + + /// <summary>The tracked locations.</summary> + public IEnumerable<LocationTracker> Locations => this.LocationDict.Values; + + /// <summary>The locations removed since the last update.</summary> + public ICollection<GameLocation> Added { get; } = new HashSet<GameLocation>(); + + /// <summary>The locations added since the last update.</summary> + public ICollection<GameLocation> Removed { get; } = new HashSet<GameLocation>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="locations">The game's list of locations.</param> + public WorldLocationsTracker(ObservableCollection<GameLocation> locations) + { + this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); + } + + /// <summary>Update the current value if needed.</summary> + public void Update() + { + // detect location changes + if (this.LocationListWatcher.IsChanged) + { + this.Remove(this.LocationListWatcher.Removed); + this.Add(this.LocationListWatcher.Added); + } + + // detect building changes + foreach (LocationTracker watcher in this.Locations.ToArray()) + { + if (watcher.BuildingsWatcher.IsChanged) + { + this.Remove(watcher.BuildingsWatcher.Removed); + this.Add(watcher.BuildingsWatcher.Added); + } + } + + // detect building interior changed (e.g. construction completed) + foreach (KeyValuePair<Building, GameLocation> pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value))) + { + GameLocation oldIndoors = pair.Value; + GameLocation newIndoors = pair.Key.indoors.Value; + + if (oldIndoors != null) + this.Added.Add(oldIndoors); + if (newIndoors != null) + this.Removed.Add(newIndoors); + } + + // update watchers + foreach (IWatcher watcher in this.Locations) + watcher.Update(); + } + + /// <summary>Set the current location list as the baseline.</summary> + public void ResetLocationList() + { + this.Removed.Clear(); + this.Added.Clear(); + this.LocationListWatcher.Reset(); + } + + /// <summary>Set the current value as the baseline.</summary> + public void Reset() + { + this.ResetLocationList(); + foreach (IWatcher watcher in this.Locations) + watcher.Reset(); + } + + /// <summary>Stop watching the player fields and release all references.</summary> + public void Dispose() + { + this.LocationListWatcher.Dispose(); + foreach (IWatcher watcher in this.Locations) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /**** + ** Enumerable wrappers + ****/ + /// <summary>Add the given buildings.</summary> + /// <param name="buildings">The buildings to add.</param> + public void Add(IEnumerable<Building> buildings) + { + foreach (Building building in buildings) + this.Add(building); + } + + /// <summary>Add the given locations.</summary> + /// <param name="locations">The locations to add.</param> + public void Add(IEnumerable<GameLocation> locations) + { + foreach (GameLocation location in locations) + this.Add(location); + } + + /// <summary>Remove the given buildings.</summary> + /// <param name="buildings">The buildings to remove.</param> + public void Remove(IEnumerable<Building> buildings) + { + foreach (Building building in buildings) + this.Remove(building); + } + + /// <summary>Remove the given locations.</summary> + /// <param name="locations">The locations to remove.</param> + public void Remove(IEnumerable<GameLocation> locations) + { + foreach (GameLocation location in locations) + this.Remove(location); + } + + /**** + ** Main add/remove logic + ****/ + /// <summary>Add the given building.</summary> + /// <param name="building">The building to add.</param> + public void Add(Building building) + { + if (building == null) + return; + + GameLocation indoors = building.indoors.Value; + this.BuildingIndoors[building] = indoors; + this.Add(indoors); + } + + /// <summary>Add the given location.</summary> + /// <param name="location">The location to add.</param> + public void Add(GameLocation location) + { + if (location == null) + return; + + // remove old location if needed + this.Remove(location); + + // track change + this.Added.Add(location); + + // add + this.LocationDict[location] = new LocationTracker(location); + if (location is BuildableGameLocation buildableLocation) + this.Add(buildableLocation.buildings); + } + + /// <summary>Remove the given building.</summary> + /// <param name="building">The building to remove.</param> + public void Remove(Building building) + { + if (building == null) + return; + + this.BuildingIndoors.Remove(building); + this.Remove(building.indoors.Value); + } + + /// <summary>Remove the given location.</summary> + /// <param name="location">The location to remove.</param> + public void Remove(GameLocation location) + { + if (location == null) + return; + + if (this.LocationDict.TryGetValue(location, out LocationTracker watcher)) + { + // track change + this.Removed.Add(location); + + // remove + this.LocationDict.Remove(location); + watcher.Dispose(); + if (location is BuildableGameLocation buildableLocation) + this.Remove(buildableLocation.buildings); + } + } + } +} |