diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-01-01 18:57:05 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-01 18:57:05 -0500 |
commit | 00932a335c86be1303ff6443b38d1db629b444fc (patch) | |
tree | 8376207a89b08b4e72c1bcbd46ecd3946e516e5c | |
parent | dca60f42b2048d6b0b27517b9e7686665e61e9c2 (diff) | |
parent | b6aef499d3c9b82d33c75039e0ed58c22618c426 (diff) | |
download | SMAPI-00932a335c86be1303ff6443b38d1db629b444fc.tar.gz SMAPI-00932a335c86be1303ff6443b38d1db629b444fc.tar.bz2 SMAPI-00932a335c86be1303ff6443b38d1db629b444fc.zip |
Merge pull request #686 from wartech0/chest-tracking
Add chest items changed event
-rw-r--r-- | docs/release-notes.md | 5 | ||||
-rw-r--r-- | src/SMAPI/Events/ChestInventoryChangedEventArgs.cs | 48 | ||||
-rw-r--r-- | src/SMAPI/Events/IWorldEvents.cs | 3 | ||||
-rw-r--r-- | src/SMAPI/Events/InventoryChangedEventArgs.cs | 34 | ||||
-rw-r--r-- | src/SMAPI/Events/ItemStackChange.cs | 20 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/EventManager.cs | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 17 | ||||
-rw-r--r-- | src/SMAPI/Framework/SnapshotItemListDiff.cs | 66 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/ChestTracker.cs | 101 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs | 143 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs | 8 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/LocationTracker.cs | 60 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 32 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs | 13 | ||||
-rw-r--r-- | src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs | 15 |
16 files changed, 504 insertions, 72 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ac95d86..a8fa2b90 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,15 +29,16 @@ * Fixed main sidebar link pointing to wiki instead of home page. * For modders: + * Added `World.ChestInventoryChanged` event (thanks to collaboration with wartech0!). * Added asset propagation for... * grass textures; * winter flooring textures; - * `Data\Bundles` changes (for added bundles only). + * `Data\Bundles` changes (for added bundles only); + * `Characters\Farmer\farmer_girl_base_bald`. * Added direct `Console` access to paranoid mode warnings. * Improved error messages for `TargetParameterCountException` when using the reflection API. * `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event). * Fixed private textures loaded from content packs not having their `Name` field set. - * Fixed asset propagation for `Characters\Farmer\farmer_girl_base_bald`. * For SMAPI developers: * You can now run local environments without configuring Amazon, Azure, MongoDB, and Pastebin accounts. diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs new file mode 100644 index 00000000..4b4c4210 --- /dev/null +++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using StardewValley; +using StardewValley.Objects; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments for a <see cref="IWorldEvents.ChestInventoryChanged"/> event.</summary> + public class ChestInventoryChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// <summary>The chest whose inventory changed.</summary> + public Chest Chest { get; } + + /// <summary>The location containing the chest.</summary> + public GameLocation Location { get; } + + /// <summary>The added item stacks.</summary> + public IEnumerable<Item> Added { get; } + + /// <summary>The removed item stacks.</summary> + public IEnumerable<Item> Removed { get; } + + /// <summary>The item stacks whose size changed.</summary> + public IEnumerable<ItemStackSizeChange> QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="chest">The chest whose inventory changed.</param> + /// <param name="location">The location containing the chest.</param> + /// <param name="added">The added item stacks.</param> + /// <param name="removed">The removed item stacks.</param> + /// <param name="quantityChanged">The item stacks whose size changed.</param> + internal ChestInventoryChangedEventArgs(Chest chest, GameLocation location, Item[] added, Item[] removed, ItemStackSizeChange[] quantityChanged) + { + this.Location = location; + this.Chest = chest; + this.Added = added; + this.Removed = removed; + this.QuantityChanged = quantityChanged; + } + } +} diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 0ceffcc1..9569a57b 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Events /// <summary>Raised after objects are added or removed in a location.</summary> event EventHandler<ObjectListChangedEventArgs> ObjectListChanged; + /// <summary>Raised after items are added or removed from a chest.</summary> + event EventHandler<ChestInventoryChangedEventArgs> ChestInventoryChanged; + /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> event EventHandler<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged; } diff --git a/src/SMAPI/Events/InventoryChangedEventArgs.cs b/src/SMAPI/Events/InventoryChangedEventArgs.cs index 874c2e48..40cd4128 100644 --- a/src/SMAPI/Events/InventoryChangedEventArgs.cs +++ b/src/SMAPI/Events/InventoryChangedEventArgs.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using StardewValley; namespace StardewModdingAPI.Events @@ -14,13 +13,13 @@ namespace StardewModdingAPI.Events /// <summary>The player whose inventory changed.</summary> public Farmer Player { get; } - /// <summary>The added items.</summary> + /// <summary>The added item stacks.</summary> public IEnumerable<Item> Added { get; } - /// <summary>The removed items.</summary> + /// <summary>The removed item stacks.</summary> public IEnumerable<Item> Removed { get; } - /// <summary>The items whose stack sizes changed, with the relative change.</summary> + /// <summary>The item stacks whose size changed.</summary> public IEnumerable<ItemStackSizeChange> QuantityChanged { get; } /// <summary>Whether the affected player is the local one.</summary> @@ -32,28 +31,15 @@ namespace StardewModdingAPI.Events *********/ /// <summary>Construct an instance.</summary> /// <param name="player">The player whose inventory changed.</param> - /// <param name="changedItems">The inventory changes.</param> - internal InventoryChangedEventArgs(Farmer player, ItemStackChange[] changedItems) + /// <param name="added">The added item stacks.</param> + /// <param name="removed">The removed item stacks.</param> + /// <param name="quantityChanged">The item stacks whose size changed.</param> + internal InventoryChangedEventArgs(Farmer player, Item[] added, Item[] removed, ItemStackSizeChange[] quantityChanged) { this.Player = player; - this.Added = changedItems - .Where(n => n.ChangeType == ChangeType.Added) - .Select(p => p.Item) - .ToArray(); - - this.Removed = changedItems - .Where(n => n.ChangeType == ChangeType.Removed) - .Select(p => p.Item) - .ToArray(); - - this.QuantityChanged = changedItems - .Where(n => n.ChangeType == ChangeType.StackChange) - .Select(change => new ItemStackSizeChange( - item: change.Item, - oldSize: change.Item.Stack - change.StackChange, - newSize: change.Item.Stack - )) - .ToArray(); + this.Added = added; + this.Removed = removed; + this.QuantityChanged = quantityChanged; } } } diff --git a/src/SMAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs deleted file mode 100644 index f9ae6df6..00000000 --- a/src/SMAPI/Events/ItemStackChange.cs +++ /dev/null @@ -1,20 +0,0 @@ -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// <summary>Represents an inventory slot that changed.</summary> - public class ItemStackChange - { - /********* - ** Accessors - *********/ - /// <summary>The item in the slot.</summary> - public Item Item { get; set; } - - /// <summary>The amount by which the item's stack size changed.</summary> - public int StackChange { get; set; } - - /// <summary>How the inventory slot changed.</summary> - public ChangeType ChangeType { get; set; } - } -}
\ No newline at end of file diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 18b00f69..892cbc7b 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -148,6 +148,9 @@ namespace StardewModdingAPI.Framework.Events /// <summary>Raised after objects are added or removed in a location.</summary> public readonly ManagedEvent<ObjectListChangedEventArgs> ObjectListChanged; + /// <summary>Raised after items are added or removed from a chest.</summary> + public readonly ManagedEvent<ChestInventoryChangedEventArgs> ChestInventoryChanged; + /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> public readonly ManagedEvent<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged; @@ -221,6 +224,7 @@ namespace StardewModdingAPI.Framework.Events this.LocationListChanged = ManageEventOf<LocationListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.NpcListChanged = ManageEventOf<NpcListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.ObjectListChanged = ManageEventOf<ObjectListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.ChestInventoryChanged = ManageEventOf<ChestInventoryChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ChestInventoryChanged)); this.TerrainFeatureListChanged = ManageEventOf<TerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index b85002a3..2ae69669 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -51,6 +51,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ObjectListChanged.Remove(value); } + /// <summary>Raised after items are added or removed from a chest.</summary> + public event EventHandler<ChestInventoryChangedEventArgs> ChestInventoryChanged + { + add => this.EventManager.ChestInventoryChanged.Add(value); + remove => this.EventManager.ChestInventoryChanged.Remove(value); + } + /// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary> public event EventHandler<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4774233e..e2b22ba7 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -702,6 +702,16 @@ namespace StardewModdingAPI.Framework if (locState.Objects.IsChanged) events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); + // chest items changed + if (events.ChestInventoryChanged.HasListeners()) + { + foreach (var pair in locState.ChestItems) + { + SnapshotItemListDiff diff = pair.Value; + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged)); + } + } + // terrain features changed if (locState.TerrainFeatures.IsChanged) events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed)); @@ -740,12 +750,13 @@ namespace StardewModdingAPI.Framework } // raise player inventory changed - ItemStackChange[] changedItems = playerState.InventoryChanges.ToArray(); - if (changedItems.Any()) + if (playerState.Inventory.IsChanged) { + var inventory = playerState.Inventory; + if (this.Monitor.IsVerbose) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, changedItems)); + events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged)); } } } diff --git a/src/SMAPI/Framework/SnapshotItemListDiff.cs b/src/SMAPI/Framework/SnapshotItemListDiff.cs new file mode 100644 index 00000000..e8ab1b1e --- /dev/null +++ b/src/SMAPI/Framework/SnapshotItemListDiff.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// <summary>A snapshot of a tracked item list.</summary> + internal class SnapshotItemListDiff + { + /********* + ** Accessors + *********/ + /// <summary>Whether the item list changed.</summary> + public bool IsChanged { get; } + + /// <summary>The removed values.</summary> + public Item[] Removed { get; } + + /// <summary>The added values.</summary> + public Item[] Added { get; } + + /// <summary>The items whose stack sizes changed.</summary> + public ItemStackSizeChange[] QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Update the snapshot.</summary> + /// <param name="added">The added values.</param> + /// <param name="removed">The removed values.</param> + /// <param name="sizesChanged">The items whose stack sizes changed.</param> + public SnapshotItemListDiff(Item[] added, Item[] removed, ItemStackSizeChange[] sizesChanged) + { + this.Removed = removed; + this.Added = added; + this.QuantityChanged = sizesChanged; + + this.IsChanged = removed.Length > 0 || added.Length > 0 || sizesChanged.Length > 0; + } + + /// <summary>Get a snapshot diff if anything changed in the given data.</summary> + /// <param name="added">The added item stacks.</param> + /// <param name="removed">The removed item stacks.</param> + /// <param name="stackSizes">The items with their previous stack sizes.</param> + /// <param name="changes">The inventory changes, or <c>null</c> if nothing changed.</param> + /// <returns>Returns whether anything changed.</returns> + public static bool TryGetChanges(ISet<Item> added, ISet<Item> removed, IDictionary<Item, int> stackSizes, out SnapshotItemListDiff changes) + { + KeyValuePair<Item, int>[] sizesChanged = stackSizes.Where(p => p.Key.Stack != p.Value).ToArray(); + if (sizesChanged.Any() || added.Any() || removed.Any()) + { + changes = new SnapshotItemListDiff( + added: added.ToArray(), + removed: removed.ToArray(), + sizesChanged: sizesChanged.Select(p => new ItemStackSizeChange(p.Key, p.Value, p.Key.Stack)).ToArray() + ); + return true; + } + + changes = null; + return false; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs new file mode 100644 index 00000000..65f58ee7 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Objects; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// <summary>Tracks changes to a chest's items.</summary> + internal class ChestTracker : IDisposable + { + /********* + ** Fields + *********/ + /// <summary>The item stack sizes as of the last update.</summary> + private readonly IDictionary<Item, int> StackSizes; + + /// <summary>Items added since the last update.</summary> + private readonly HashSet<Item> Added = new HashSet<Item>(new ObjectReferenceComparer<Item>()); + + /// <summary>Items removed since the last update.</summary> + private readonly HashSet<Item> Removed = new HashSet<Item>(new ObjectReferenceComparer<Item>()); + + /// <summary>The underlying inventory watcher.</summary> + private readonly ICollectionWatcher<Item> InventoryWatcher; + + + /********* + ** Accessors + *********/ + /// <summary>The chest being tracked.</summary> + public Chest Chest { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="chest">The chest being tracked.</param> + public ChestTracker(Chest chest) + { + this.Chest = chest; + this.InventoryWatcher = WatcherFactory.ForNetList(chest.items); + + this.StackSizes = this.Chest.items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); + } + + /// <summary>Update the current values if needed.</summary> + public void Update() + { + // update watcher + this.InventoryWatcher.Update(); + foreach (Item item in this.InventoryWatcher.Added) + this.Added.Add(item); + foreach (Item item in this.InventoryWatcher.Removed) + { + if (!this.Added.Remove(item)) // item didn't change if it was both added and removed, so remove it from both lists + this.Removed.Add(item); + } + + // stop tracking removed stacks + foreach (Item item in this.Removed) + this.StackSizes.Remove(item); + } + + /// <summary>Reset all trackers so their current values are the baseline.</summary> + public void Reset() + { + // update stack sizes + foreach (Item item in this.StackSizes.Keys.ToArray().Concat(this.Added)) + this.StackSizes[item] = item.Stack; + + // update watcher + this.InventoryWatcher.Reset(); + this.Added.Clear(); + this.Removed.Clear(); + } + + /// <summary>Get the inventory changes since the last update, if anything changed.</summary> + /// <param name="changes">The inventory changes, or <c>null</c> if nothing changed.</param> + /// <returns>Returns whether anything changed.</returns> + public bool TryGetInventoryChanges(out SnapshotItemListDiff changes) + { + return SnapshotItemListDiff.TryGetChanges(added: this.Added, removed: this.Removed, stackSizes: this.StackSizes, out changes); + } + + /// <summary>Release watchers and resources.</summary> + public void Dispose() + { + this.StackSizes.Clear(); + this.Added.Clear(); + this.Removed.Clear(); + this.InventoryWatcher.Dispose(); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs new file mode 100644 index 00000000..0b4d3030 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using Netcode; +using StardewModdingAPI.Framework.StateTracking.Comparers; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// <summary>A watcher which detects changes to a net list field.</summary> + /// <typeparam name="TValue">The list value type.</typeparam> + internal class NetListWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue> + where TValue : class, INetObject<INetSerializable> + { + /********* + ** Fields + *********/ + /// <summary>The field being watched.</summary> + private readonly NetList<TValue, NetRef<TValue>> Field; + + /// <summary>The pairs added since the last reset.</summary> + private readonly ISet<TValue> AddedImpl = new HashSet<TValue>(new ObjectReferenceComparer<TValue>()); + + /// <summary>The pairs removed since the last reset.</summary> + private readonly ISet<TValue> RemovedImpl = new HashSet<TValue>(new ObjectReferenceComparer<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 NetListWatcher(NetList<TValue, NetRef<TValue>> field) + { + this.Field = field; + field.OnElementChanged += this.OnElementChanged; + field.OnArrayReplaced += this.OnArrayReplaced; + } + + /// <summary>Set the current value as the baseline.</summary> + public void Reset() + { + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// <summary>Update the current value if needed.</summary> + public void Update() + { + this.AssertNotDisposed(); + } + + /// <summary>Stop watching the field and release all references.</summary> + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnElementChanged -= this.OnElementChanged; + this.Field.OnArrayReplaced -= this.OnArrayReplaced; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>A callback invoked when the value list is replaced.</summary> + /// <param name="list">The net field whose values changed.</param> + /// <param name="oldValues">The previous list of values.</param> + /// <param name="newValues">The new list of values.</param> + private void OnArrayReplaced(NetList<TValue, NetRef<TValue>> list, IList<TValue> oldValues, IList<TValue> newValues) + { + ISet<TValue> oldSet = new HashSet<TValue>(oldValues, new ObjectReferenceComparer<TValue>()); + ISet<TValue> changed = new HashSet<TValue>(newValues, new ObjectReferenceComparer<TValue>()); + + foreach (TValue value in oldSet) + { + if (!changed.Contains(value)) + this.Remove(value); + } + foreach (TValue value in changed) + { + if (!oldSet.Contains(value)) + this.Add(value); + } + } + + /// <summary>A callback invoked when an entry is replaced.</summary> + /// <param name="list">The net field whose values changed.</param> + /// <param name="index">The list index which changed.</param> + /// <param name="oldValue">The previous value.</param> + /// <param name="newValue">The new value.</param> + private void OnElementChanged(NetList<TValue, NetRef<TValue>> list, int index, TValue oldValue, TValue newValue) + { + this.Remove(oldValue); + this.Add(newValue); + } + + /// <summary>Track an added item.</summary> + /// <param name="value">The value that was added.</param> + private void Add(TValue value) + { + if (value == null) + return; + + if (this.RemovedImpl.Contains(value)) + { + this.AddedImpl.Remove(value); + this.RemovedImpl.Remove(value); + } + else + this.AddedImpl.Add(value); + } + + /// <summary>Track a removed item.</summary> + /// <param name="value">The value that was removed.</param> + private void Remove(TValue value) + { + if (value == null) + return; + + if (this.AddedImpl.Contains(value)) + { + this.AddedImpl.Remove(value); + this.RemovedImpl.Remove(value); + } + else + this.RemovedImpl.Add(value); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index 314ff7f5..bde43486 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -82,6 +82,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new NetCollectionWatcher<T>(collection); } + /// <summary>Get a watcher for a net list.</summary> + /// <typeparam name="T">The value type.</typeparam> + /// <param name="collection">The net list.</param> + public static ICollectionWatcher<T> ForNetList<T>(NetList<T, NetRef<T>> collection) where T : class, INetObject<INetSerializable> + { + return new NetListWatcher<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 1f479e12..519fe8f4 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -5,8 +5,9 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; +using StardewValley.Objects; using StardewValley.TerrainFeatures; -using Object = StardewValley.Object; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking { @@ -42,11 +43,14 @@ namespace StardewModdingAPI.Framework.StateTracking public ICollectionWatcher<NPC> NpcsWatcher { get; } /// <summary>Tracks added or removed objects.</summary> - public IDictionaryWatcher<Vector2, Object> ObjectsWatcher { get; } + public IDictionaryWatcher<Vector2, SObject> ObjectsWatcher { get; } /// <summary>Tracks added or removed terrain features.</summary> public IDictionaryWatcher<Vector2, TerrainFeature> TerrainFeaturesWatcher { get; } + /// <summary>Tracks items added or removed to chests.</summary> + public IDictionary<Vector2, ChestTracker> ChestWatchers { get; } = new Dictionary<Vector2, ChestTracker>(); + /********* ** Public methods @@ -74,13 +78,8 @@ namespace StardewModdingAPI.Framework.StateTracking this.ObjectsWatcher, this.TerrainFeaturesWatcher }); - } - /// <summary>Stop watching the player fields and release all references.</summary> - public void Dispose() - { - foreach (IWatcher watcher in this.Watchers) - watcher.Dispose(); + this.UpdateChestWatcherList(added: location.Objects.Pairs, removed: new KeyValuePair<Vector2, SObject>[0]); } /// <summary>Update the current value if needed.</summary> @@ -88,6 +87,11 @@ namespace StardewModdingAPI.Framework.StateTracking { foreach (IWatcher watcher in this.Watchers) watcher.Update(); + + this.UpdateChestWatcherList(added: this.ObjectsWatcher.Added, removed: this.ObjectsWatcher.Removed); + + foreach (var watcher in this.ChestWatchers) + watcher.Value.Update(); } /// <summary>Set the current value as the baseline.</summary> @@ -95,6 +99,46 @@ namespace StardewModdingAPI.Framework.StateTracking { foreach (IWatcher watcher in this.Watchers) watcher.Reset(); + + foreach (var watcher in this.ChestWatchers) + watcher.Value.Reset(); + } + + /// <summary>Stop watching the player fields and release all references.</summary> + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + + foreach (var watcher in this.ChestWatchers.Values) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Update the watcher list for added or removed chests.</summary> + /// <param name="added">The objects added to the location.</param> + /// <param name="removed">The objects removed from the location.</param> + private void UpdateChestWatcherList(IEnumerable<KeyValuePair<Vector2, SObject>> added, IEnumerable<KeyValuePair<Vector2, SObject>> removed) + { + // remove unused watchers + foreach (KeyValuePair<Vector2, SObject> pair in removed) + { + if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ChestTracker watcher)) + { + watcher.Dispose(); + this.ChestWatchers.Remove(pair.Key); + } + } + + // add new watchers + foreach (KeyValuePair<Vector2, SObject> pair in added) + { + if (pair.Value is Chest chest && !this.ChestWatchers.ContainsKey(pair.Key)) + this.ChestWatchers.Add(pair.Key, new ChestTracker(chest)); + } } } } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 6302a889..cf49a7c1 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -2,10 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Enums; -using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; -using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking { @@ -99,25 +98,32 @@ namespace StardewModdingAPI.Framework.StateTracking return this.Player.currentLocation ?? this.LastValidLocation; } - /// <summary>Get the player inventory changes between two states.</summary> - public IEnumerable<ItemStackChange> GetInventoryChanges() + /// <summary>Get the inventory changes since the last update, if anything changed.</summary> + /// <param name="changes">The inventory changes, or <c>null</c> if nothing changed.</param> + /// <returns>Returns whether anything changed.</returns> + public bool TryGetInventoryChanges(out SnapshotItemListDiff changes) { - IDictionary<Item, int> previous = this.PreviousInventory; IDictionary<Item, int> current = this.GetInventory(); - foreach (Item item in previous.Keys.Union(current.Keys)) + + ISet<Item> added = new HashSet<Item>(new ObjectReferenceComparer<Item>()); + ISet<Item> removed = new HashSet<Item>(new ObjectReferenceComparer<Item>()); + foreach (Item item in this.PreviousInventory.Keys.Union(current.Keys)) { - if (!previous.TryGetValue(item, out int prevStack)) - yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; - else if (!current.TryGetValue(item, out int newStack)) - yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed }; - else if (prevStack != newStack) - yield return new ItemStackChange { Item = item, StackChange = newStack - prevStack, ChangeType = ChangeType.StackChange }; + if (!this.PreviousInventory.ContainsKey(item)) + added.Add(item); + else if (!current.ContainsKey(item)) + removed.Add(item); } + + return SnapshotItemListDiff.TryGetChanges(added: added, removed: removed, stackSizes: this.PreviousInventory, out changes); } - /// <summary>Stop watching the player fields and release all references.</summary> + /// <summary>Release watchers and resources.</summary> public void Dispose() { + this.PreviousInventory.Clear(); + this.CurrentInventory?.Clear(); + foreach (IWatcher watcher in this.Watchers) watcher.Dispose(); } diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index d3029540..6ae52fd0 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Buildings; +using StardewValley.Objects; using StardewValley.TerrainFeatures; namespace StardewModdingAPI.Framework.StateTracking.Snapshots @@ -33,6 +34,9 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots /// <summary>Tracks added or removed terrain features.</summary> public SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>> TerrainFeatures { get; } = new SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>>(); + /// <summary>Tracks changed chest inventories.</summary> + public IDictionary<Chest, SnapshotItemListDiff> ChestItems { get; } = new Dictionary<Chest, SnapshotItemListDiff>(); + /********* ** Public methods @@ -48,12 +52,21 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots /// <param name="watcher">The watcher to snapshot.</param> public void Update(LocationTracker watcher) { + // main lists 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); + + // chest inventories + this.ChestItems.Clear(); + foreach (ChestTracker tracker in watcher.ChestWatchers.Values) + { + if (tracker.TryGetInventoryChanges(out SnapshotItemListDiff changes)) + this.ChestItems[tracker.Chest] = changes; + } } } } diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs index 7bcd9f82..f0fb9485 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs @@ -11,6 +11,13 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots internal class PlayerSnapshot { /********* + ** Fields + *********/ + /// <summary>An empty item list diff.</summary> + private readonly SnapshotItemListDiff EmptyItemListDiff = new SnapshotItemListDiff(new Item[0], new Item[0], new ItemStackSizeChange[0]); + + + /********* ** Accessors *********/ /// <summary>The player being tracked.</summary> @@ -27,7 +34,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots .ToDictionary(skill => skill, skill => new SnapshotDiff<int>()); /// <summary>Get a list of inventory changes.</summary> - public IEnumerable<ItemStackChange> InventoryChanges { get; private set; } + public SnapshotItemListDiff Inventory { get; private set; } /********* @@ -47,7 +54,11 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.Location.Update(watcher.LocationWatcher); foreach (var pair in this.Skills) pair.Value.Update(watcher.SkillWatchers[pair.Key]); - this.InventoryChanges = watcher.GetInventoryChanges().ToArray(); + + this.Inventory = watcher.TryGetInventoryChanges(out SnapshotItemListDiff itemChanges) + ? itemChanges + : this.EmptyItemListDiff; + } } } |