using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; namespace StardewModdingAPI.Framework.StateTracking { /// Tracks changes to a player's data. internal class PlayerTracker : IDisposable { /********* ** Fields *********/ /// 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(); /********* ** Accessors *********/ /// The player being tracked. public Farmer Player { get; } /// The player's current location. public IValueWatcher LocationWatcher { get; } /// Tracks changes to the player's skill levels. public IDictionary> SkillWatchers { get; } /********* ** Public methods *********/ /// Construct an instance. /// The player to track. public PlayerTracker(Farmer player) { // init player data this.Player = player; this.CurrentInventory = this.GetInventory(); this.PreviousInventory = new Dictionary(this.CurrentInventory); // init trackers this.LocationWatcher = WatcherFactory.ForReference($"player.{nameof(player.currentLocation)}", this.GetCurrentLocation); this.SkillWatchers = new Dictionary> { [SkillType.Combat] = WatcherFactory.ForNetValue($"player.{nameof(player.combatLevel)}", player.combatLevel), [SkillType.Farming] = WatcherFactory.ForNetValue($"player.{nameof(player.farmingLevel)}", player.farmingLevel), [SkillType.Fishing] = WatcherFactory.ForNetValue($"player.{nameof(player.fishingLevel)}", player.fishingLevel), [SkillType.Foraging] = WatcherFactory.ForNetValue($"player.{nameof(player.foragingLevel)}", player.foragingLevel), [SkillType.Luck] = WatcherFactory.ForNetValue($"player.{nameof(player.luckLevel)}", player.luckLevel), [SkillType.Mining] = WatcherFactory.ForNetValue($"player.{nameof(player.miningLevel)}", player.miningLevel) }; // track watchers for convenience this.Watchers.Add(this.LocationWatcher); 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(); // 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 inventory changes since the last update, if anything changed. /// The inventory changes, or null if nothing changed. /// Returns whether anything changed. public bool TryGetInventoryChanges([NotNullWhen(true)] out SnapshotItemListDiff? changes) { IDictionary current = this.GetInventory(); ISet added = new HashSet(new ObjectReferenceComparer()); ISet removed = new HashSet(new ObjectReferenceComparer()); foreach (Item item in this.PreviousInventory.Keys.Union(current.Keys)) { 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); } /// Release watchers and resources. public void Dispose() { this.PreviousInventory.Clear(); this.CurrentInventory.Clear(); 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); } } }