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); } } }