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