summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-05-05 01:31:06 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-05-05 01:31:06 -0400
commitb8fd3aedfe884741bdda8c68398427f875585456 (patch)
tree12b6395923c930fa3155d4be2f38b1aa6e660217 /src/SMAPI/Framework
parenta65a49a62201cc897e73c265a0a808ef0baad002 (diff)
downloadSMAPI-b8fd3aedfe884741bdda8c68398427f875585456.tar.gz
SMAPI-b8fd3aedfe884741bdda8c68398427f875585456.tar.bz2
SMAPI-b8fd3aedfe884741bdda8c68398427f875585456.zip
rewrite location events for multiplayer
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs22
-rw-r--r--src/SMAPI/Framework/SGame.cs158
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs93
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs8
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs21
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs32
-rw-r--r--src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs221
7 files changed, 425 insertions, 130 deletions
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index 9030ba97..84036127 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -108,17 +108,13 @@ namespace StardewModdingAPI.Framework.Events
/****
** LocationEvents
****/
- /// <summary>Raised after the player warps to a new location.</summary>
- public readonly ManagedEvent<EventArgsCurrentLocationChanged> Location_CurrentLocationChanged;
-
/// <summary>Raised after a game location is added or removed.</summary>
- public readonly ManagedEvent<EventArgsGameLocationsChanged> Location_LocationsChanged;
+ public readonly ManagedEvent<EventArgsLocationsChanged> Location_LocationsChanged;
- /// <summary>Raised after the list of objects in the current location changes (e.g. an object is added or removed).</summary>
- [Obsolete]
- public readonly ManagedEvent<EventArgsLocationObjectsChanged> Location_LocationObjectsChanged;
+ /// <summary>Raised after buildings are added or removed in a location.</summary>
+ public readonly ManagedEvent<EventArgsLocationBuildingsChanged> Location_BuildingsChanged;
- /// <summary>Raised after the list of objects in a location changes (e.g. an object is added or removed).</summary>
+ /// <summary>Raised after objects are added or removed in a location.</summary>
public readonly ManagedEvent<EventArgsLocationObjectsChanged> Location_ObjectsChanged;
/****
@@ -160,6 +156,10 @@ namespace StardewModdingAPI.Framework.Events
/// <summary> Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed.</summary>
public readonly ManagedEvent<EventArgsLevelUp> Player_LeveledUp;
+ /// <summary>Raised after the player warps to a new location.</summary>
+ public readonly ManagedEvent<EventArgsPlayerWarped> Player_Warped;
+
+
/****
** SaveEvents
****/
@@ -241,9 +241,8 @@ namespace StardewModdingAPI.Framework.Events
this.Input_ButtonPressed = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonPressed));
this.Input_ButtonReleased = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonReleased));
- this.Location_CurrentLocationChanged = ManageEventOf<EventArgsCurrentLocationChanged>(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged));
- this.Location_LocationsChanged = ManageEventOf<EventArgsGameLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged));
- this.Location_LocationObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged));
+ this.Location_LocationsChanged = ManageEventOf<EventArgsLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged));
+ this.Location_BuildingsChanged = ManageEventOf<EventArgsLocationBuildingsChanged>(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged));
this.Location_ObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged));
this.Menu_Changed = ManageEventOf<EventArgsClickableMenuChanged>(nameof(MenuEvents), nameof(MenuEvents.MenuChanged));
@@ -258,6 +257,7 @@ namespace StardewModdingAPI.Framework.Events
this.Player_InventoryChanged = ManageEventOf<EventArgsInventoryChanged>(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged));
this.Player_LeveledUp = ManageEventOf<EventArgsLevelUp>(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp));
+ this.Player_Warped = ManageEventOf<EventArgsPlayerWarped>(nameof(PlayerEvents), nameof(PlayerEvents.Warped));
this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate));
this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate));
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index d2ba85f8..c8c30834 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -23,7 +23,6 @@ using StardewValley.Menus;
using StardewValley.Tools;
using xTile.Dimensions;
using xTile.Layers;
-using SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework
{
@@ -92,11 +91,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Tracks changes to the save ID.</summary>
private readonly IValueWatcher<ulong> SaveIdWatcher;
- /// <summary>Tracks changes to the location list.</summary>
- private readonly ICollectionWatcher<GameLocation> LocationListWatcher;
-
- /// <summary>Tracks changes to each location.</summary>
- private readonly IDictionary<GameLocation, LocationTracker> LocationWatchers = new Dictionary<GameLocation, LocationTracker>();
+ /// <summary>Tracks changes to the game's locations.</summary>
+ private readonly WorldLocationsTracker LocationsWatcher;
/// <summary>Tracks changes to <see cref="Game1.activeClickableMenu"/>.</summary>
private readonly IValueWatcher<IClickableMenu> ActiveMenuWatcher;
@@ -165,14 +161,14 @@ namespace StardewModdingAPI.Framework
this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
- this.LocationListWatcher = WatcherFactory.ForObservableCollection((ObservableCollection<GameLocation>)Game1.locations);
+ this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations);
this.Watchers.AddRange(new IWatcher[]
{
this.SaveIdWatcher,
this.WindowSizeWatcher,
this.TimeWatcher,
this.ActiveMenuWatcher,
- this.LocationListWatcher
+ this.LocationsWatcher
});
}
@@ -351,22 +347,7 @@ namespace StardewModdingAPI.Framework
foreach (IWatcher watcher in this.Watchers)
watcher.Update();
this.CurrentPlayerTracker?.Update();
-
- // update location watchers
- if (this.LocationListWatcher.IsChanged)
- {
- foreach (GameLocation location in this.LocationListWatcher.Removed.Union(this.LocationListWatcher.Added))
- {
- if (this.LocationWatchers.TryGetValue(location, out LocationTracker watcher))
- {
- this.Watchers.Remove(watcher);
- this.LocationWatchers.Remove(location);
- watcher.Dispose();
- }
- }
- foreach (GameLocation location in this.LocationListWatcher.Added)
- this.LocationWatchers[location] = new LocationTracker(location);
- }
+ this.LocationsWatcher.Update();
/*********
** Locale changed events
@@ -516,45 +497,86 @@ namespace StardewModdingAPI.Framework
*********/
if (Context.IsWorldReady)
{
- // update player info
- PlayerTracker curPlayer = this.CurrentPlayerTracker;
+ bool raiseWorldEvents = !this.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded
- // raise current location changed
- if (curPlayer.TryGetNewLocation(out GameLocation newLocation))
+ // raise location changes
+ if (this.LocationsWatcher.IsChanged)
{
- if (this.VerboseLogging)
- this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace);
- this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(curPlayer.LocationWatcher.PreviousValue, newLocation));
+ // location list changes
+ if (this.LocationsWatcher.IsLocationListChanged)
+ {
+ GameLocation[] added = this.LocationsWatcher.Added.ToArray();
+ GameLocation[] removed = this.LocationsWatcher.Removed.ToArray();
+ this.LocationsWatcher.ResetLocationList();
+
+ if (this.VerboseLogging)
+ {
+ string addedText = this.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
+ string removedText = this.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
+ this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace);
+ }
+
+ this.Events.Location_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed));
+ }
+
+ // raise location contents changed
+ if (raiseWorldEvents)
+ {
+ foreach (LocationTracker watcher in this.LocationsWatcher.Locations)
+ {
+ // objects changed
+ if (watcher.ObjectsWatcher.IsChanged)
+ {
+ GameLocation location = watcher.Location;
+ var added = watcher.ObjectsWatcher.Added;
+ var removed = watcher.ObjectsWatcher.Removed;
+ watcher.ObjectsWatcher.Reset();
+
+ this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed));
+ }
+
+ // buildings changed
+ if (watcher.BuildingsWatcher.IsChanged)
+ {
+ GameLocation location = watcher.Location;
+ var added = watcher.BuildingsWatcher.Added;
+ var removed = watcher.BuildingsWatcher.Removed;
+ watcher.BuildingsWatcher.Reset();
+
+ this.Events.Location_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed));
+ }
+ }
+ }
+ else
+ this.LocationsWatcher.Reset();
}
- // raise location list changed
- if (this.LocationListWatcher.IsChanged)
+ // raise time changed
+ if (raiseWorldEvents && this.TimeWatcher.IsChanged)
{
+ int was = this.TimeWatcher.PreviousValue;
+ int now = this.TimeWatcher.CurrentValue;
+ this.TimeWatcher.Reset();
+
if (this.VerboseLogging)
- {
- string added = this.LocationListWatcher.Added.Any() ? string.Join(", ", this.LocationListWatcher.Added.Select(p => p.Name)) : "none";
- string removed = this.LocationListWatcher.Removed.Any() ? string.Join(", ", this.LocationListWatcher.Removed.Select(p => p.Name)) : "none";
- this.Monitor.Log($"Context: location list changed (added {added}; removed {removed}).", LogLevel.Trace);
- }
+ this.Monitor.Log($"Context: time changed from {was} to {now}.", LogLevel.Trace);
- this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations));
+ this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now));
}
+ else
+ this.TimeWatcher.Reset();
- // raise events that shouldn't be triggered on initial load
- if (!this.SaveIdWatcher.IsChanged)
+ // raise player events
+ if (raiseWorldEvents)
{
- // raise location objects changed
- foreach (LocationTracker watcher in this.LocationWatchers.Values)
- {
- if (watcher.LocationObjectsWatcher.IsChanged)
- {
- GameLocation location = watcher.Location;
- var added = watcher.LocationObjectsWatcher.Added;
- var removed = watcher.LocationObjectsWatcher.Removed;
- watcher.Reset();
+ PlayerTracker curPlayer = this.CurrentPlayerTracker;
- this.Events.Location_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed, watcher.Location.netObjects.FieldDict));
- }
+ // raise current location changed
+ if (curPlayer.TryGetNewLocation(out GameLocation newLocation))
+ {
+ if (this.VerboseLogging)
+ this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace);
+ this.Events.Player_Warped.Raise(new EventArgsPlayerWarped(curPlayer.LocationWatcher.PreviousValue, newLocation));
}
// raise player leveled up a skill
@@ -574,27 +596,6 @@ namespace StardewModdingAPI.Framework
this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems.ToList()));
}
- // raise current location's object list changed
- {
- if (curPlayer.TryGetLocationChanges(out IDictionaryWatcher<Vector2, SObject> watcher))
- {
- if (this.VerboseLogging)
- this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace);
-
- GameLocation location = curPlayer.GetCurrentLocation();
-
- this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, watcher.Added, watcher.Removed, location.netObjects.FieldDict));
- }
- }
-
- // raise time changed
- if (this.TimeWatcher.IsChanged)
- {
- if (this.VerboseLogging)
- this.Monitor.Log($"Context: time changed from {this.TimeWatcher.PreviousValue} to {this.TimeWatcher.CurrentValue}.", LogLevel.Trace);
- this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.TimeWatcher.PreviousValue, this.TimeWatcher.CurrentValue));
- }
-
// raise mine level changed
if (curPlayer.TryGetNewMineLevel(out int mineLevel))
{
@@ -603,18 +604,11 @@ namespace StardewModdingAPI.Framework
this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(curPlayer.MineLevelWatcher.PreviousValue, mineLevel));
}
}
+ this.CurrentPlayerTracker?.Reset();
}
- // update state
- this.CurrentPlayerTracker?.Reset();
- this.LocationListWatcher.Reset();
+ // update save ID watcher
this.SaveIdWatcher.Reset();
- this.TimeWatcher.Reset();
- if (!Context.IsWorldReady)
- {
- foreach (LocationTracker watcher in this.LocationWatchers.Values)
- watcher.Reset();
- }
/*********
** Game update
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
new file mode 100644
index 00000000..f92edb90
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using Netcode;
+
+namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
+{
+ /// <summary>A watcher which detects changes to a Netcode collection.</summary>
+ internal class NetCollectionWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
+ where TValue : INetObject<INetSerializable>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The field being watched.</summary>
+ private readonly NetCollection<TValue> Field;
+
+ /// <summary>The pairs added since the last reset.</summary>
+ private readonly List<TValue> AddedImpl = new List<TValue>();
+
+ /// <summary>The pairs demoved since the last reset.</summary>
+ private readonly List<TValue> RemovedImpl = new List<TValue>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the collection changed since the last reset.</summary>
+ public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0;
+
+ /// <summary>The values added since the last reset.</summary>
+ public IEnumerable<TValue> Added => this.AddedImpl;
+
+ /// <summary>The values removed since the last reset.</summary>
+ public IEnumerable<TValue> Removed => this.RemovedImpl;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="field">The field to watch.</param>
+ public NetCollectionWatcher(NetCollection<TValue> field)
+ {
+ this.Field = field;
+ field.OnValueAdded += this.OnValueAdded;
+ field.OnValueRemoved += this.OnValueRemoved;
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.AssertNotDisposed();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.AssertNotDisposed();
+
+ this.AddedImpl.Clear();
+ this.RemovedImpl.Clear();
+ }
+
+ /// <summary>Stop watching the field and release all references.</summary>
+ public override void Dispose()
+ {
+ if (!this.IsDisposed)
+ {
+ this.Field.OnValueAdded -= this.OnValueAdded;
+ this.Field.OnValueRemoved -= this.OnValueRemoved;
+ }
+
+ base.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>A callback invoked when an entry is added to the collection.</summary>
+ /// <param name="value">The added value.</param>
+ private void OnValueAdded(TValue value)
+ {
+ this.AddedImpl.Add(value);
+ }
+
+ /// <summary>A callback invoked when an entry is removed from the collection.</summary>
+ /// <param name="value">The added value.</param>
+ private void OnValueRemoved(TValue value)
+ {
+ this.RemovedImpl.Add(value);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
index bf261bb5..a4982faa 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
@@ -36,6 +36,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
return new ObservableCollectionWatcher<T>(collection);
}
+ /// <summary>Get a watcher for a net collection.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="collection">The net collection.</param>
+ public static NetCollectionWatcher<T> ForNetCollection<T>(NetCollection<T> collection) where T : INetObject<INetSerializable>
+ {
+ return new NetCollectionWatcher<T>(collection);
+ }
+
/// <summary>Get a watcher for a net dictionary.</summary>
/// <typeparam name="TKey">The dictionary key type.</typeparam>
/// <typeparam name="TValue">The dictionary value type.</typeparam>
diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs
index 8cf4e7a2..07570401 100644
--- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs
@@ -1,8 +1,11 @@
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Locations;
using Object = StardewValley.Object;
namespace StardewModdingAPI.Framework.StateTracking
@@ -26,8 +29,11 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>The tracked location.</summary>
public GameLocation Location { get; }
- /// <summary>Tracks changes to the current location's objects.</summary>
- public IDictionaryWatcher<Vector2, Object> LocationObjectsWatcher { get; }
+ /// <summary>Tracks changes to the location's buildings.</summary>
+ public ICollectionWatcher<Building> BuildingsWatcher { get; }
+
+ /// <summary>Tracks changes to the location's objects.</summary>
+ public IDictionaryWatcher<Vector2, Object> ObjectsWatcher { get; }
/*********
@@ -40,10 +46,15 @@ namespace StardewModdingAPI.Framework.StateTracking
this.Location = location;
// init watchers
- this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects);
- this.Watchers.AddRange(new[]
+ this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects);
+ this.BuildingsWatcher = location is BuildableGameLocation buildableLocation
+ ? WatcherFactory.ForNetCollection(buildableLocation.buildings)
+ : (ICollectionWatcher<Building>)WatcherFactory.ForObservableCollection(new ObservableCollection<Building>());
+
+ this.Watchers.AddRange(new IWatcher[]
{
- this.LocationObjectsWatcher
+ this.BuildingsWatcher,
+ this.ObjectsWatcher
});
}
diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
index 032705b7..dea2e30d 100644
--- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs
@@ -1,12 +1,10 @@
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 SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework.StateTracking
{
@@ -38,9 +36,6 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>The player's current location.</summary>
public IValueWatcher<GameLocation> LocationWatcher { get; }
- /// <summary>Tracks changes to the player's current location's objects.</summary>
- public IDictionaryWatcher<Vector2, SObject> LocationObjectsWatcher { get; private set; }
-
/// <summary>The player's current mine level.</summary>
public IValueWatcher<int> MineLevelWatcher { get; }
@@ -61,7 +56,6 @@ namespace StardewModdingAPI.Framework.StateTracking
// init trackers
this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation);
- this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects);
this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0);
this.SkillWatchers = new Dictionary<EventArgsLevelUp.LevelType, IValueWatcher<int>>
{
@@ -77,7 +71,6 @@ namespace StardewModdingAPI.Framework.StateTracking
this.Watchers.AddRange(new IWatcher[]
{
this.LocationWatcher,
- this.LocationObjectsWatcher,
this.MineLevelWatcher
});
this.Watchers.AddRange(this.SkillWatchers.Values);
@@ -93,16 +86,6 @@ namespace StardewModdingAPI.Framework.StateTracking
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().netObjects);
- this.Watchers.Add(this.LocationObjectsWatcher);
- }
-
// update inventory
this.CurrentInventory = this.GetInventory();
}
@@ -154,21 +137,6 @@ namespace StardewModdingAPI.Framework.StateTracking
return this.LocationWatcher.IsChanged;
}
- /// <summary>Get object changes to the player's current location if they there as of the last reset.</summary>
- /// <param name="watcher">The object change watcher.</param>
- /// <returns>Returns whether it changed.</returns>
- public bool TryGetLocationChanges(out IDictionaryWatcher<Vector2, SObject> watcher)
- {
- if (this.LocationWatcher.IsChanged)
- {
- watcher = null;
- return false;
- }
-
- watcher = this.LocationObjectsWatcher;
- return watcher.IsChanged;
- }
-
/// <summary>Get the player's new mine level if it changed.</summary>
/// <param name="mineLevel">The player's current mine level.</param>
/// <returns>Returns whether it changed.</returns>
diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
new file mode 100644
index 00000000..d9090c08
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
@@ -0,0 +1,221 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
+using StardewValley;
+using StardewValley.Buildings;
+using StardewValley.Locations;
+
+namespace StardewModdingAPI.Framework.StateTracking
+{
+ /// <summary>Detects changes to the game's locations.</summary>
+ internal class WorldLocationsTracker : IWatcher
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Tracks changes to the location list.</summary>
+ private readonly ICollectionWatcher<GameLocation> LocationListWatcher;
+
+ /// <summary>A lookup of the tracked locations.</summary>
+ private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>();
+
+ /// <summary>A lookup of registered buildings and their indoor location.</summary>
+ private readonly IDictionary<Building, GameLocation> BuildingIndoors = new Dictionary<Building, GameLocation>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether locations were added or removed since the last reset.</summary>
+ public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any();
+
+ /// <summary>Whether any tracked location data changed since the last reset.</summary>
+ public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged);
+
+ /// <summary>The tracked locations.</summary>
+ public IEnumerable<LocationTracker> Locations => this.LocationDict.Values;
+
+ /// <summary>The locations removed since the last update.</summary>
+ public ICollection<GameLocation> Added { get; } = new HashSet<GameLocation>();
+
+ /// <summary>The locations added since the last update.</summary>
+ public ICollection<GameLocation> Removed { get; } = new HashSet<GameLocation>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locations">The game's list of locations.</param>
+ public WorldLocationsTracker(ObservableCollection<GameLocation> locations)
+ {
+ this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ // detect location changes
+ if (this.LocationListWatcher.IsChanged)
+ {
+ this.Remove(this.LocationListWatcher.Removed);
+ this.Add(this.LocationListWatcher.Added);
+ }
+
+ // detect building changes
+ foreach (LocationTracker watcher in this.Locations.ToArray())
+ {
+ if (watcher.BuildingsWatcher.IsChanged)
+ {
+ this.Remove(watcher.BuildingsWatcher.Removed);
+ this.Add(watcher.BuildingsWatcher.Added);
+ }
+ }
+
+ // detect building interior changed (e.g. construction completed)
+ foreach (KeyValuePair<Building, GameLocation> 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);
+ }
+
+ // update watchers
+ foreach (IWatcher watcher in this.Locations)
+ watcher.Update();
+ }
+
+ /// <summary>Set the current location list as the baseline.</summary>
+ public void ResetLocationList()
+ {
+ this.Removed.Clear();
+ this.Added.Clear();
+ this.LocationListWatcher.Reset();
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.ResetLocationList();
+ foreach (IWatcher watcher in this.Locations)
+ watcher.Reset();
+ }
+
+ /// <summary>Stop watching the player fields and release all references.</summary>
+ public void Dispose()
+ {
+ this.LocationListWatcher.Dispose();
+ foreach (IWatcher watcher in this.Locations)
+ watcher.Dispose();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /****
+ ** Enumerable wrappers
+ ****/
+ /// <summary>Add the given buildings.</summary>
+ /// <param name="buildings">The buildings to add.</param>
+ public void Add(IEnumerable<Building> buildings)
+ {
+ foreach (Building building in buildings)
+ this.Add(building);
+ }
+
+ /// <summary>Add the given locations.</summary>
+ /// <param name="locations">The locations to add.</param>
+ public void Add(IEnumerable<GameLocation> locations)
+ {
+ foreach (GameLocation location in locations)
+ this.Add(location);
+ }
+
+ /// <summary>Remove the given buildings.</summary>
+ /// <param name="buildings">The buildings to remove.</param>
+ public void Remove(IEnumerable<Building> buildings)
+ {
+ foreach (Building building in buildings)
+ this.Remove(building);
+ }
+
+ /// <summary>Remove the given locations.</summary>
+ /// <param name="locations">The locations to remove.</param>
+ public void Remove(IEnumerable<GameLocation> locations)
+ {
+ foreach (GameLocation location in locations)
+ this.Remove(location);
+ }
+
+ /****
+ ** Main add/remove logic
+ ****/
+ /// <summary>Add the given building.</summary>
+ /// <param name="building">The building to add.</param>
+ public void Add(Building building)
+ {
+ if (building == null)
+ return;
+
+ GameLocation indoors = building.indoors.Value;
+ this.BuildingIndoors[building] = indoors;
+ this.Add(indoors);
+ }
+
+ /// <summary>Add the given location.</summary>
+ /// <param name="location">The location to add.</param>
+ public void Add(GameLocation location)
+ {
+ if (location == null)
+ return;
+
+ // remove old location if needed
+ this.Remove(location);
+
+ // track change
+ this.Added.Add(location);
+
+ // add
+ this.LocationDict[location] = new LocationTracker(location);
+ if (location is BuildableGameLocation buildableLocation)
+ this.Add(buildableLocation.buildings);
+ }
+
+ /// <summary>Remove the given building.</summary>
+ /// <param name="building">The building to remove.</param>
+ public void Remove(Building building)
+ {
+ if (building == null)
+ return;
+
+ this.BuildingIndoors.Remove(building);
+ this.Remove(building.indoors.Value);
+ }
+
+ /// <summary>Remove the given location.</summary>
+ /// <param name="location">The location to remove.</param>
+ 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);
+ }
+ }
+ }
+}