From eead352af26d0fcc5cac147d0eb5ec384854d931 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Apr 2018 20:37:17 -0400 Subject: rewrite world/player state tracking (#453) --- .../StateTracking/Comparers/EquatableComparer.cs | 32 ++++ .../Comparers/ObjectReferenceComparer.cs | 29 +++ .../FieldWatchers/BaseDisposableWatcher.cs | 36 ++++ .../FieldWatchers/ComparableWatcher.cs | 62 +++++++ .../FieldWatchers/NetDictionaryWatcher.cs | 103 +++++++++++ .../StateTracking/FieldWatchers/NetValueWatcher.cs | 83 +++++++++ .../FieldWatchers/ObservableCollectionWatcher.cs | 86 +++++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 54 ++++++ .../Framework/StateTracking/ICollectionWatcher.cs | 17 ++ .../Framework/StateTracking/IDictionaryWatcher.cs | 7 + src/SMAPI/Framework/StateTracking/IValueWatcher.cs | 15 ++ src/SMAPI/Framework/StateTracking/IWatcher.cs | 24 +++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 202 +++++++++++++++++++++ 13 files changed, 750 insertions(+) create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs create mode 100644 src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs create mode 100644 src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IValueWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/IWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/PlayerTracker.cs (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs new file mode 100644 index 00000000..a96ffdb6 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// Compares instances using . + /// The value type. + internal class EquatableComparer : IEqualityComparer where T : IEquatable + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + if (x == null) + return y == null; + return x.Equals(y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs new file mode 100644 index 00000000..ef9adafb --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// A comparer which considers two references equal if they point to the same instance. + /// The value type. + internal class ObjectReferenceComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + return object.ReferenceEquals(x, y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs new file mode 100644 index 00000000..40ec6c57 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// The base implementation for a disposable watcher. + internal abstract class BaseDisposableWatcher : IDisposable + { + /********* + ** Properties + *********/ + /// Whether the watcher has been disposed. + protected bool IsDisposed { get; private set; } + + + /********* + ** Public methods + *********/ + /// Stop watching the field and release all references. + public virtual void Dispose() + { + this.IsDisposed = true; + } + + + /********* + ** Protected methods + *********/ + /// Throw an exception if the watcher is disposed. + /// The watcher is disposed. + protected void AssertNotDisposed() + { + if (this.IsDisposed) + throw new ObjectDisposedException(this.GetType().Name); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs new file mode 100644 index 00000000..d51fc2ac --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a value using a specified instance. + internal class ComparableWatcher : IValueWatcher + { + /********* + ** Properties + *********/ + /// Get the current value. + private readonly Func GetValue; + + /// The equality comparer. + private readonly IEqualityComparer Comparer; + + + /********* + ** Accessors + *********/ + /// The field value at the last reset. + public T PreviousValue { get; private set; } + + /// The latest value. + public T CurrentValue { get; private set; } + + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Get the current value. + /// The equality comparer which indicates whether two values are the same. + public ComparableWatcher(Func getValue, IEqualityComparer comparer) + { + this.GetValue = getValue; + this.Comparer = comparer; + this.PreviousValue = getValue(); + } + + /// Update the current value if needed. + public void Update() + { + this.CurrentValue = this.GetValue(); + this.IsChanged = !this.Comparer.Equals(this.PreviousValue, this.CurrentValue); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Release any references if needed when the field is no longer needed. + public void Dispose() { } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs new file mode 100644 index 00000000..7a2bf84e --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net dictionary field. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + internal class NetDictionaryWatcher : BaseDisposableWatcher, IDictionaryWatcher + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + /********* + ** Properties + *********/ + /// The pairs added since the last reset. + private readonly IDictionary PairsAdded = new Dictionary(); + + /// The pairs demoved since the last reset. + private readonly IDictionary PairsRemoved = new Dictionary(); + + /// The field being watched. + private readonly NetDictionary Field; + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.PairsAdded.Count > 0 || this.PairsRemoved.Count > 0; + + /// The values added since the last reset. + public IEnumerable> Added => this.PairsAdded; + + /// The values removed since the last reset. + public IEnumerable> Removed => this.PairsRemoved; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetDictionaryWatcher(NetDictionary field) + { + this.Field = field; + + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PairsAdded.Clear(); + this.PairsRemoved.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the dictionary. + /// The entry key. + /// The entry value. + private void OnValueAdded(TKey key, TValue value) + { + this.PairsAdded[key] = value; + } + + /// A callback invoked when an entry is removed from the dictionary. + /// The entry key. + /// The entry value. + private void OnValueRemoved(TKey key, TValue value) + { + if (!this.PairsRemoved.ContainsKey(key)) + this.PairsRemoved[key] = value; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs new file mode 100644 index 00000000..188ed9f3 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs @@ -0,0 +1,83 @@ +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net value field. + internal class NetValueWatcher : BaseDisposableWatcher, IValueWatcher where TSelf : NetFieldBase + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetFieldBase Field; + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + /// The field value at the last reset. + public T PreviousValue { get; private set; } + + /// The latest value. + public T CurrentValue { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetValueWatcher(NetFieldBase field) + { + this.Field = field; + this.PreviousValue = field.Value; + this.CurrentValue = field.Value; + + field.fieldChangeVisibleEvent += this.OnValueChanged; + field.fieldChangeEvent += this.OnValueChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.fieldChangeEvent -= this.OnValueChanged; + this.Field.fieldChangeVisibleEvent -= this.OnValueChanged; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when the field's value changes. + /// The field being watched. + /// The old field value. + /// The new field value. + private void OnValueChanged(TSelf field, T oldValue, T newValue) + { + this.CurrentValue = newValue; + this.IsChanged = true; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs new file mode 100644 index 00000000..34a97097 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to an observable collection. + internal class ObservableCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly ObservableCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public ObservableCollectionWatcher(ObservableCollection field) + { + this.Field = field; + field.CollectionChanged += this.OnCollectionChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + this.Field.CollectionChanged -= this.OnCollectionChanged; + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added or removed from the collection. + /// The event sender. + /// The event arguments. + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null) + this.AddedImpl.AddRange(e.NewItems.Cast()); + if (e.OldItems != null) + this.RemovedImpl.AddRange(e.OldItems.Cast()); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs new file mode 100644 index 00000000..bf261bb5 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Netcode; +using StardewModdingAPI.Framework.StateTracking.Comparers; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// Provides convenience wrappers for creating watchers. + internal static class WatcherFactory + { + /********* + ** Public methods + *********/ + /// Get a watcher for an value. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForEquatable(Func getValue) where T : IEquatable + { + return new ComparableWatcher(getValue, new EquatableComparer()); + } + + /// Get a watcher which detects when an object reference changes. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForReference(Func getValue) + { + return new ComparableWatcher(getValue, new ObjectReferenceComparer()); + } + + /// Get a watcher for an observable collection. + /// The value type. + /// The observable collection. + public static ObservableCollectionWatcher ForObservableCollection(ObservableCollection collection) + { + return new ObservableCollectionWatcher(collection); + } + + /// Get a watcher for a net dictionary. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + /// The net field. + public static NetDictionaryWatcher ForNetDictionary(NetDictionary field) + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + return new NetDictionaryWatcher(field); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs new file mode 100644 index 00000000..7a7759e3 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a collection. + internal interface ICollectionWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The values added since the last reset. + IEnumerable Added { get; } + + /// The values removed since the last reset. + IEnumerable Removed { get; } + } +} diff --git a/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs new file mode 100644 index 00000000..691ed377 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a dictionary. + internal interface IDictionaryWatcher : ICollectionWatcher> { } +} diff --git a/src/SMAPI/Framework/StateTracking/IValueWatcher.cs b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs new file mode 100644 index 00000000..4afca972 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a value. + internal interface IValueWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The field value at the last reset. + T PreviousValue { get; } + + /// The latest value. + T CurrentValue { get; } + } +} diff --git a/src/SMAPI/Framework/StateTracking/IWatcher.cs b/src/SMAPI/Framework/StateTracking/IWatcher.cs new file mode 100644 index 00000000..8c7fa51c --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/IWatcher.cs @@ -0,0 +1,24 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which detects changes to something. + internal interface IWatcher : IDisposable + { + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + bool IsChanged { get; } + + + /********* + ** Methods + *********/ + /// Update the current value if needed. + void Update(); + + /// Set the current value as the baseline. + void Reset(); + } +} diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs new file mode 100644 index 00000000..81e074ec --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -0,0 +1,202 @@ +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 SFarmer = StardewValley.Farmer; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a player's data. + internal class PlayerTracker : IDisposable + { + /********* + ** Properties + *********/ + /// The player's inventory as of the last reset. + private IDictionary PreviousInventory; + + /// The player's inventory change as of the last update. + private IDictionary CurrentInventory; + + /// The player's last valid location. + private GameLocation LastValidLocation; + + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// The player being tracked. + public SFarmer Player { get; } + + /// The player's current location. + public IValueWatcher LocationWatcher { get; } + + /// Tracks changes to the player's current location's objects. + public IDictionaryWatcher LocationObjectsWatcher { get; private set; } + + /// The player's current mine level. + public IValueWatcher MineLevelWatcher { get; } + + /// Tracks changes to the player's skill levels. + public IDictionary> SkillWatchers { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to track. + public PlayerTracker(SFarmer player) + { + // init player data + this.Player = player; + this.PreviousInventory = this.GetInventory(); + + // init trackers + this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); + this.SkillWatchers = new Dictionary> + { + [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForEquatable(() => player.combatLevel), + [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForEquatable(() => player.farmingLevel), + [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForEquatable(() => player.fishingLevel), + [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForEquatable(() => player.foragingLevel), + [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForEquatable(() => player.luckLevel), + [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForEquatable(() => player.miningLevel) + }; + + // track watchers for convenience + this.Watchers.AddRange(new IWatcher[] + { + this.LocationWatcher, + this.LocationObjectsWatcher, + this.MineLevelWatcher + }); + this.Watchers.AddRange(this.SkillWatchers.Values); + } + + /// Update the current values if needed. + public void Update() + { + // update valid location + this.LastValidLocation = this.GetCurrentLocation(); + + // update watchers + 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().objects); + this.Watchers.Add(this.LocationObjectsWatcher); + } + + // update inventory + this.CurrentInventory = this.GetInventory(); + } + + /// Reset all trackers so their current values are the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + + this.PreviousInventory = this.CurrentInventory; + } + + /// Get the player's current location, ignoring temporary null values. + /// The game will set to null in some cases, e.g. when they're a secondary player in multiplayer and transition to a location that hasn't been synced yet. While that's happening, this returns the player's last valid location instead. + public GameLocation GetCurrentLocation() + { + return this.Player.currentLocation ?? this.LastValidLocation; + } + + /// Get the player inventory changes between two states. + public IEnumerable GetInventoryChanges() + { + IDictionary previous = this.PreviousInventory; + IDictionary current = this.GetInventory(); + foreach (Item item in previous.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 }; + } + } + + /// 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 object changes to the player's current location if they there as of the last reset. + /// The object change watcher. + /// Returns whether it changed. + public bool TryGetLocationChanges(out IDictionaryWatcher watcher) + { + if (this.LocationWatcher.IsChanged) + { + watcher = null; + return false; + } + + watcher = this.LocationObjectsWatcher; + return watcher.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() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get the player's current inventory. + private IDictionary GetInventory() + { + return this.Player.Items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); + } + } +} -- cgit From ae956d0ad8847a8b61d964bd40238de75768260a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 19:25:34 -0400 Subject: update for SDV 1.3.0.38 (#453) --- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 81e074ec..f6bfc98e 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { @@ -100,7 +100,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.Remove(this.LocationObjectsWatcher); this.LocationObjectsWatcher.Dispose(); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.Watchers.Add(this.LocationObjectsWatcher); } -- cgit From beb2f9c1484626e03acfecffd0f90cd2cddb8cc6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Apr 2018 19:59:41 -0400 Subject: remove alias no longer needed in SDV 1.3 (#453) --- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index f6bfc98e..032705b7 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -6,7 +6,6 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; -using SFarmer = StardewValley.Farmer; using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking @@ -34,7 +33,7 @@ namespace StardewModdingAPI.Framework.StateTracking ** Accessors *********/ /// The player being tracked. - public SFarmer Player { get; } + public Farmer Player { get; } /// The player's current location. public IValueWatcher LocationWatcher { get; } @@ -54,7 +53,7 @@ namespace StardewModdingAPI.Framework.StateTracking *********/ /// Construct an instance. /// The player to track. - public PlayerTracker(SFarmer player) + public PlayerTracker(Farmer player) { // init player data this.Player = player; -- cgit From 8051862c7bd2fe498657eef4bb102b5ca33390a6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 May 2018 20:44:20 -0400 Subject: add LocationEvents.ObjectsChanged event --- .../Framework/StateTracking/LocationTracker.cs | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/SMAPI/Framework/StateTracking/LocationTracker.cs (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs new file mode 100644 index 00000000..8cf4e7a2 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a location's data. + internal class LocationTracker : IWatcher + { + /********* + ** Properties + *********/ + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged => this.Watchers.Any(p => p.IsChanged); + + /// The tracked location. + public GameLocation Location { get; } + + /// Tracks changes to the current location's objects. + public IDictionaryWatcher LocationObjectsWatcher { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location to track. + public LocationTracker(GameLocation location) + { + this.Location = location; + + // init watchers + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.Watchers.AddRange(new[] + { + this.LocationObjectsWatcher + }); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + /// Update the current value if needed. + public void Update() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + } + + /// Set the current value as the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + } + } +} -- cgit From b8fd3aedfe884741bdda8c68398427f875585456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 01:31:06 -0400 Subject: rewrite location events for multiplayer --- .../FieldWatchers/NetCollectionWatcher.cs | 93 +++++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 8 + .../Framework/StateTracking/LocationTracker.cs | 21 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 32 --- .../StateTracking/WorldLocationsTracker.cs | 221 +++++++++++++++++++++ 5 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs (limited to 'src/SMAPI/Framework/StateTracking') 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 +{ + /// A watcher which detects changes to a Netcode collection. + internal class NetCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + where TValue : INetObject + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetCollectionWatcher(NetCollection field) + { + this.Field = field; + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the collection. + /// The added value. + private void OnValueAdded(TValue value) + { + this.AddedImpl.Add(value); + } + + /// A callback invoked when an entry is removed from the collection. + /// The added value. + 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(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net collection. + public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : INetObject + { + return new NetCollectionWatcher(collection); + } + /// Get a watcher for a net dictionary. /// The dictionary key type. /// The dictionary value type. 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 /// The tracked location. public GameLocation Location { get; } - /// Tracks changes to the current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; } + /// Tracks changes to the location's buildings. + public ICollectionWatcher BuildingsWatcher { get; } + + /// Tracks changes to the location's objects. + public IDictionaryWatcher 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)WatcherFactory.ForObservableCollection(new ObservableCollection()); + + 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 /// The player's current location. public IValueWatcher LocationWatcher { get; } - /// Tracks changes to the player's current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; private set; } - /// The player's current mine level. public IValueWatcher 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> { @@ -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; } - /// Get object changes to the player's current location if they there as of the last reset. - /// The object change watcher. - /// Returns whether it changed. - public bool TryGetLocationChanges(out IDictionaryWatcher watcher) - { - if (this.LocationWatcher.IsChanged) - { - watcher = null; - return false; - } - - watcher = this.LocationObjectsWatcher; - return watcher.IsChanged; - } - /// Get the player's new mine level if it changed. /// The player's current mine level. /// Returns whether it changed. 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 +{ + /// Detects changes to the game's locations. + internal class WorldLocationsTracker : IWatcher + { + /********* + ** Properties + *********/ + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationListWatcher; + + /// A lookup of the tracked locations. + private IDictionary LocationDict { get; } = new Dictionary(); + + /// A lookup of registered buildings and their indoor location. + private readonly IDictionary BuildingIndoors = new Dictionary(); + + + /********* + ** Accessors + *********/ + /// Whether locations were added or removed since the last reset. + public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any(); + + /// Whether any tracked location data changed since the last reset. + public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged); + + /// The tracked locations. + public IEnumerable Locations => this.LocationDict.Values; + + /// The locations removed since the last update. + public ICollection Added { get; } = new HashSet(); + + /// The locations added since the last update. + public ICollection Removed { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game's list of locations. + public WorldLocationsTracker(ObservableCollection locations) + { + this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); + } + + /// Update the current value if needed. + 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 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(); + } + + /// Set the current location list as the baseline. + public void ResetLocationList() + { + this.Removed.Clear(); + this.Added.Clear(); + this.LocationListWatcher.Reset(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.ResetLocationList(); + foreach (IWatcher watcher in this.Locations) + watcher.Reset(); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + this.LocationListWatcher.Dispose(); + foreach (IWatcher watcher in this.Locations) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /**** + ** Enumerable wrappers + ****/ + /// Add the given buildings. + /// The buildings to add. + public void Add(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Add(building); + } + + /// Add the given locations. + /// The locations to add. + public void Add(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Add(location); + } + + /// Remove the given buildings. + /// The buildings to remove. + public void Remove(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Remove(building); + } + + /// Remove the given locations. + /// The locations to remove. + public void Remove(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Remove(location); + } + + /**** + ** Main add/remove logic + ****/ + /// Add the given building. + /// The building to add. + public void Add(Building building) + { + if (building == null) + return; + + GameLocation indoors = building.indoors.Value; + this.BuildingIndoors[building] = indoors; + this.Add(indoors); + } + + /// Add the given location. + /// The location to add. + 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); + } + + /// Remove the given building. + /// The building to remove. + public void Remove(Building building) + { + if (building == null) + return; + + this.BuildingIndoors.Remove(building); + this.Remove(building.indoors.Value); + } + + /// Remove the given location. + /// The location to remove. + 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); + } + } + } +} -- cgit From 9d3e37317893126a9e0598f8f850abd23cad36c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 16:14:41 -0400 Subject: update for Stardew Valley 1.3.11 (#521) --- .../Framework/StateTracking/FieldWatchers/WatcherFactory.cs | 9 +++++++++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework/StateTracking') diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index a4982faa..4f1ac9f4 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -36,6 +36,15 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new ObservableCollectionWatcher(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net field instance type. + /// The net collection. + public static NetValueWatcher ForNetValue(NetFieldBase field) where TSelf : NetFieldBase + { + return new NetValueWatcher(field); + } + /// Get a watcher for a net collection. /// The value type. /// The net collection. diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index dea2e30d..3814e534 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -59,12 +59,12 @@ namespace StardewModdingAPI.Framework.StateTracking this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { - [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForEquatable(() => player.combatLevel), - [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForEquatable(() => player.farmingLevel), - [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForEquatable(() => player.fishingLevel), - [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForEquatable(() => player.foragingLevel), - [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForEquatable(() => player.luckLevel), - [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForEquatable(() => player.miningLevel) + [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), + [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), + [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), + [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), + [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), + [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) }; // track watchers for convenience -- cgit From b3f116