using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using StardewModdingAPI.Framework.StateTracking.Comparers; 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 { /********* ** Fields *********/ /// Tracks changes to the location list. private readonly ICollectionWatcher LocationListWatcher; /// Tracks changes to the list of active mine locations. private readonly ICollectionWatcher MineLocationListWatcher; /// A lookup of the tracked locations. private IDictionary LocationDict { get; } = new Dictionary(new ObjectReferenceComparer()); /// A lookup of registered buildings and their indoor location. private readonly IDictionary BuildingIndoors = new Dictionary(new ObjectReferenceComparer()); /********* ** 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(new ObjectReferenceComparer()); /// The locations added since the last update. public ICollection Removed { get; } = new HashSet(new ObjectReferenceComparer()); /********* ** Public methods *********/ /// Construct an instance. /// The game's list of locations. /// The game's list of active mine locations. public WorldLocationsTracker(ObservableCollection locations, IList activeMineLocations) { this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations); } /// Update the current value if needed. public void Update() { // update watchers this.LocationListWatcher.Update(); this.MineLocationListWatcher.Update(); foreach (LocationTracker watcher in this.Locations) watcher.Update(); // detect added/removed locations if (this.LocationListWatcher.IsChanged) { this.Remove(this.LocationListWatcher.Removed); this.Add(this.LocationListWatcher.Added); } if (this.MineLocationListWatcher.IsChanged) { this.Remove(this.MineLocationListWatcher.Removed); this.Add(this.MineLocationListWatcher.Added); } // detect building changed foreach (LocationTracker watcher in this.Locations.Where(p => p.BuildingsWatcher.IsChanged).ToArray()) { this.Remove(watcher.BuildingsWatcher.Removed); this.Add(watcher.BuildingsWatcher.Added); } // detect building interiors 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); } } /// Set the current location list as the baseline. public void ResetLocationList() { this.Removed.Clear(); this.Added.Clear(); this.LocationListWatcher.Reset(); this.MineLocationListWatcher.Reset(); } /// Set the current value as the baseline. public void Reset() { this.ResetLocationList(); foreach (IWatcher watcher in this.GetWatchers()) watcher.Reset(); } /// Get whether the given location is tracked. /// The location to check. public bool HasLocationTracker(GameLocation location) { return this.LocationDict.ContainsKey(location); } /// Stop watching the player fields and release all references. public void Dispose() { foreach (IWatcher watcher in this.GetWatchers()) 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); // add location this.Added.Add(location); this.LocationDict[location] = new LocationTracker(location); // add buildings 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); } } /**** ** Helpers ****/ /// The underlying watchers. private IEnumerable GetWatchers() { yield return this.LocationListWatcher; yield return this.MineLocationListWatcher; foreach (LocationTracker watcher in this.Locations) yield return watcher; } } }