using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Locations; using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking { /// 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 Farmer Player { get; } /// The player's current location. public IValueWatcher LocationWatcher { get; } /// The player's current mine level. public IValueWatcher MineLevelWatcher { get; } /// Tracks changes to the player's skill levels. public IDictionary> SkillWatchers { get; } /********* ** Public methods *********/ /// Construct an instance. /// The player to track. public PlayerTracker(Farmer player) { // init player data this.Player = player; this.PreviousInventory = this.GetInventory(); // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { [SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), [SkillType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), [SkillType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), [SkillType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), [SkillType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), [SkillType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) }; // track watchers for convenience this.Watchers.AddRange(new IWatcher[] { this.LocationWatcher, 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(); // 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 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); } } }