From eead352af26d0fcc5cac147d0eb5ec384854d931 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Apr 2018 20:37:17 -0400 Subject: rewrite world/player state tracking (#453) --- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 202 +++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/SMAPI/Framework/StateTracking/PlayerTracker.cs (limited to 'src/SMAPI/Framework/StateTracking/PlayerTracker.cs') diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs new file mode 100644 index 00000000..81e074ec --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -0,0 +1,202 @@ +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); + } + } +} -- cgit From ae956d0ad8847a8b61d964bd40238de75768260a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 Apr 2018 19:25:34 -0400 Subject: update for SDV 1.3.0.38 (#453) --- src/SMAPI/Framework/SGame.cs | 2 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework/StateTracking/PlayerTracker.cs') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 182b90fc..7ea83957 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -533,7 +533,7 @@ namespace StardewModdingAPI.Framework if (this.VerboseLogging) this.Monitor.Log("Context: current location objects changed.", LogLevel.Trace); - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().objects.FieldDict)); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(curPlayer.GetCurrentLocation().netObjects.FieldDict)); } // raise time changed diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 81e074ec..f6bfc98e 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { @@ -100,7 +100,7 @@ namespace StardewModdingAPI.Framework.StateTracking this.Watchers.Remove(this.LocationObjectsWatcher); this.LocationObjectsWatcher.Dispose(); - this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().objects); + this.LocationObjectsWatcher = WatcherFactory.ForNetDictionary(this.GetCurrentLocation().netObjects); this.Watchers.Add(this.LocationObjectsWatcher); } -- cgit From beb2f9c1484626e03acfecffd0f90cd2cddb8cc6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Apr 2018 19:59:41 -0400 Subject: remove alias no longer needed in SDV 1.3 (#453) --- .../NetFieldAnalyzerTests.cs | 3 +-- .../Framework/Commands/Player/SetLevelCommand.cs | 15 +++++++-------- src/SMAPI/Framework/SGame.cs | 11 +++++------ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 5 ++--- 4 files changed, 15 insertions(+), 19 deletions(-) (limited to 'src/SMAPI/Framework/StateTracking/PlayerTracker.cs') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 7b410085..ab3f2c5e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -19,7 +19,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests using StardewValley; using Netcode; using SObject = StardewValley.Object; - using SFarmer = StardewValley.Farmer; namespace SampleMod { @@ -34,7 +33,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests "; /// The line number where the unit tested code is injected into . - private const int SampleCodeLine = 14; + private const int SampleCodeLine = 13; /// The column number where the unit tested code is injected into . private const int SampleCodeColumn = 25; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs index 68891267..97a36066 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetLevelCommand.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using StardewValley; -using SFarmer = StardewValley.Farmer; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { @@ -11,7 +10,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ** Properties *********/ /// The experience points needed to reach each level. - /// Derived from . + /// Derived from . private readonly IDictionary LevelExp = new Dictionary { [0] = 0, @@ -52,37 +51,37 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { case "luck": Game1.player.LuckLevel = level; - Game1.player.experiencePoints[SFarmer.luckSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.luckSkill] = this.LevelExp[level]; monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info); break; case "mining": Game1.player.MiningLevel = level; - Game1.player.experiencePoints[SFarmer.miningSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.miningSkill] = this.LevelExp[level]; monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info); break; case "combat": Game1.player.CombatLevel = level; - Game1.player.experiencePoints[SFarmer.combatSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.combatSkill] = this.LevelExp[level]; monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info); break; case "farming": Game1.player.FarmingLevel = level; - Game1.player.experiencePoints[SFarmer.farmingSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.farmingSkill] = this.LevelExp[level]; monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info); break; case "fishing": Game1.player.FishingLevel = level; - Game1.player.experiencePoints[SFarmer.fishingSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.fishingSkill] = this.LevelExp[level]; monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info); break; case "foraging": Game1.player.ForagingLevel = level; - Game1.player.experiencePoints[SFarmer.foragingSkill] = this.LevelExp[level]; + Game1.player.experiencePoints[Farmer.foragingSkill] = this.LevelExp[level]; monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info); break; } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 89bf18ef..7578473b 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 SFarmer = StardewValley.Farmer; using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework @@ -872,10 +871,10 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); Game1.currentLocation.drawWater(Game1.spriteBatch); - IEnumerable source = Game1.currentLocation.farmers; + IEnumerable source = Game1.currentLocation.farmers; if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) - source = (IEnumerable)Game1.currentLocation.currentEvent.farmerActors; - IEnumerable farmers = source.Where((Func)(farmer => + source = (IEnumerable)Game1.currentLocation.currentEvent.farmerActors; + IEnumerable farmers = source.Where((Func)(farmer => { if (!farmer.IsLocalPlayer) return !(bool)((NetFieldBase)farmer.hidden); @@ -899,7 +898,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (SFarmer farmer in farmers) + foreach (Farmer farmer in farmers) { if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) { @@ -943,7 +942,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } - foreach (SFarmer farmer in farmers) + foreach (Farmer farmer in farmers) { if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) { diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index f6bfc98e..032705b7 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -6,7 +6,6 @@ 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 @@ -34,7 +33,7 @@ namespace StardewModdingAPI.Framework.StateTracking ** Accessors *********/ /// The player being tracked. - public SFarmer Player { get; } + public Farmer Player { get; } /// The player's current location. public IValueWatcher LocationWatcher { get; } @@ -54,7 +53,7 @@ namespace StardewModdingAPI.Framework.StateTracking *********/ /// Construct an instance. /// The player to track. - public PlayerTracker(SFarmer player) + public PlayerTracker(Farmer player) { // init player data this.Player = player; -- cgit From b8fd3aedfe884741bdda8c68398427f875585456 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 May 2018 01:31:06 -0400 Subject: rewrite location events for multiplayer --- docs/release-notes.md | 12 +- .../Events/EventArgsCurrentLocationChanged.cs | 31 --- src/SMAPI/Events/EventArgsGameLocationsChanged.cs | 27 --- src/SMAPI/Events/EventArgsIntChanged.cs | 3 +- .../Events/EventArgsLocationBuildingsChanged.cs | 39 ++++ .../Events/EventArgsLocationObjectsChanged.cs | 18 +- src/SMAPI/Events/EventArgsLocationsChanged.cs | 33 +++ src/SMAPI/Events/EventArgsPlayerWarped.cs | 32 +++ src/SMAPI/Events/LocationEvents.cs | 20 +- src/SMAPI/Events/PlayerEvents.cs | 10 +- src/SMAPI/Framework/Events/EventManager.cs | 22 +- src/SMAPI/Framework/SGame.cs | 158 +++++++-------- .../FieldWatchers/NetCollectionWatcher.cs | 93 +++++++++ .../StateTracking/FieldWatchers/WatcherFactory.cs | 8 + .../Framework/StateTracking/LocationTracker.cs | 21 +- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 32 --- .../StateTracking/WorldLocationsTracker.cs | 221 +++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 7 +- 18 files changed, 566 insertions(+), 221 deletions(-) delete mode 100644 src/SMAPI/Events/EventArgsCurrentLocationChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsGameLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsLocationsChanged.cs create mode 100644 src/SMAPI/Events/EventArgsPlayerWarped.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs create mode 100644 src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs (limited to 'src/SMAPI/Framework/StateTracking/PlayerTracker.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 00ed6e9c..ece388c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,7 +12,11 @@ * For modders: * Added code analysis to mod build config package to flag common issues as warnings. - * Added `LocationEvents.ObjectsChanged`, raised when an object is added/removed in any location. + * Replaced `LocationEvents` with a more powerful set of events for multiplayer: + * now raised for all locations; + * now includes added/removed building interiors; + * each event now provides a list of added/removed values; + * added buildings-changed event. * Added `Context.IsMultiplayer` and `Context.IsMainPlayer` flags. * Added `Constants.TargetPlatform` which says whether the game is running on Linux, Mac, or Windows. * Added `semanticVersion.IsPrerelease()` method. @@ -21,8 +25,10 @@ * Fixed assets not reloaded consistently when the player switches language. * Fixed console command input not saved to the log. * Fixed `helper.ModRegistry.GetApi` interface validation errors not mentioning which interface caused the issue. - * **Breaking change**: dropped some deprecated APIs. - * **Breaking change**: mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). + * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): + * dropped some deprecated APIs; + * `LocationEvents` have been rewritten (see above); + * mods can't intercept chatbox input, including the game's hotkeys to toggle the chatbox (default `T` and `?`). * In console commands: * Added `player_add name`, which lets you add items to your inventory by name instead of ID. diff --git a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs deleted file mode 100644 index 25d3ebf3..00000000 --- a/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsCurrentLocationChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's current location. - public GameLocation NewLocation { get; } - - /// The player's previous location. - public GameLocation PriorLocation { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's previous location. - /// The player's current location. - public EventArgsCurrentLocationChanged(GameLocation priorLocation, GameLocation newLocation) - { - this.NewLocation = newLocation; - this.PriorLocation = priorLocation; - } - } -} diff --git a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs deleted file mode 100644 index 78ba38fa..00000000 --- a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsGameLocationsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The current list of game locations. - public IList NewLocations { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current list of game locations. - public EventArgsGameLocationsChanged(IList newLocations) - { - this.NewLocations = newLocations; - } - } -} diff --git a/src/SMAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs index 0c742d12..a018695c 100644 --- a/src/SMAPI/Events/EventArgsIntChanged.cs +++ b/src/SMAPI/Events/EventArgsIntChanged.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace StardewModdingAPI.Events { @@ -14,6 +14,7 @@ namespace StardewModdingAPI.Events /// The current value. public int NewInt { get; } + /********* ** Public methods *********/ diff --git a/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs new file mode 100644 index 00000000..e8184ebe --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationBuildingsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public EventArgsLocationBuildingsChanged(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index a6138ddb..410ef6e6 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -8,7 +8,7 @@ using SObject = StardewValley.Object; namespace StardewModdingAPI.Events { - /// Event arguments for a or event. + /// Event arguments for a event. public class EventArgsLocationObjectsChanged : EventArgs { /********* @@ -17,31 +17,25 @@ namespace StardewModdingAPI.Events /// The location which changed. public GameLocation Location { get; } - /// The objects added to the list. + /// The objects added to the location. public IEnumerable> Added { get; } - /// The objects removed from the list. + /// The objects removed from the location. public IEnumerable> Removed { get; } - /// The current list of objects in the current location. - [Obsolete("Use " + nameof(EventArgsLocationObjectsChanged.Added))] - public IDictionary> NewObjects { get; } - /********* ** Public methods *********/ /// Construct an instance. /// The location which changed. - /// The objects added to the list. - /// The objects removed from the list. - /// The current list of objects in the current location. - public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed, IDictionary> newObjects) + /// The objects added to the location. + /// The objects removed from the location. + public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed) { this.Location = location; this.Added = added.ToArray(); this.Removed = removed.ToArray(); - this.NewObjects = newObjects; } } } diff --git a/src/SMAPI/Events/EventArgsLocationsChanged.cs b/src/SMAPI/Events/EventArgsLocationsChanged.cs new file mode 100644 index 00000000..20984f45 --- /dev/null +++ b/src/SMAPI/Events/EventArgsLocationsChanged.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public EventArgsLocationsChanged(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/EventArgsPlayerWarped.cs b/src/SMAPI/Events/EventArgsPlayerWarped.cs new file mode 100644 index 00000000..93026aea --- /dev/null +++ b/src/SMAPI/Events/EventArgsPlayerWarped.cs @@ -0,0 +1,32 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsPlayerWarped : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's previous location. + public GameLocation PriorLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's previous location. + /// The player's current location. + public EventArgsPlayerWarped(GameLocation priorLocation, GameLocation newLocation) + { + this.NewLocation = newLocation; + this.PriorLocation = priorLocation; + } + } +} diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index 3ed09136..ff75c619 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -16,29 +16,21 @@ namespace StardewModdingAPI.Events /********* ** Events *********/ - /// Raised after the player warps to a new location. - public static event EventHandler CurrentLocationChanged - { - add => LocationEvents.EventManager.Location_CurrentLocationChanged.Add(value); - remove => LocationEvents.EventManager.Location_CurrentLocationChanged.Remove(value); - } - /// Raised after a game location is added or removed. - public static event EventHandler LocationsChanged + public static event EventHandler LocationsChanged { add => LocationEvents.EventManager.Location_LocationsChanged.Add(value); remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value); } - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - [Obsolete("Use " + nameof(LocationEvents) + "." + nameof(LocationEvents.ObjectsChanged) + " instead")] - public static event EventHandler LocationObjectsChanged + /// Raised after buildings are added or removed in a location. + public static event EventHandler BuildingsChanged { - add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); - remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); + add => LocationEvents.EventManager.Location_BuildingsChanged.Add(value); + remove => LocationEvents.EventManager.Location_BuildingsChanged.Remove(value); } - /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + /// Raised after objects are added or removed in a location. public static event EventHandler ObjectsChanged { add => LocationEvents.EventManager.Location_ObjectsChanged.Add(value); diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs index 84a7ff63..6e7050e3 100644 --- a/src/SMAPI/Events/PlayerEvents.cs +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -23,13 +23,21 @@ namespace StardewModdingAPI.Events remove => PlayerEvents.EventManager.Player_InventoryChanged.Remove(value); } - /// 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. + /// 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. public static event EventHandler LeveledUp { add => PlayerEvents.EventManager.Player_LeveledUp.Add(value); remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value); } + /// Raised after the player warps to a new location. + public static event EventHandler Warped + { + add => PlayerEvents.EventManager.Player_Warped.Add(value); + remove => PlayerEvents.EventManager.Player_Warped.Remove(value); + } + + /********* ** Public methods 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 ****/ - /// Raised after the player warps to a new location. - public readonly ManagedEvent Location_CurrentLocationChanged; - /// Raised after a game location is added or removed. - public readonly ManagedEvent Location_LocationsChanged; + public readonly ManagedEvent Location_LocationsChanged; - /// Raised after the list of objects in the current location changes (e.g. an object is added or removed). - [Obsolete] - public readonly ManagedEvent Location_LocationObjectsChanged; + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent Location_BuildingsChanged; - /// Raised after the list of objects in a location changes (e.g. an object is added or removed). + /// Raised after objects are added or removed in a location. public readonly ManagedEvent Location_ObjectsChanged; /**** @@ -160,6 +156,10 @@ namespace StardewModdingAPI.Framework.Events /// 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. public readonly ManagedEvent Player_LeveledUp; + /// Raised after the player warps to a new location. + public readonly ManagedEvent Player_Warped; + + /**** ** SaveEvents ****/ @@ -241,9 +241,8 @@ namespace StardewModdingAPI.Framework.Events this.Input_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); this.Input_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - this.Location_CurrentLocationChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); - this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Location_LocationObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + this.Location_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Location_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); this.Location_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); this.Menu_Changed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); @@ -258,6 +257,7 @@ namespace StardewModdingAPI.Framework.Events this.Player_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); this.Player_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + this.Player_Warped = ManageEventOf(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 /// Tracks changes to the save ID. private readonly IValueWatcher SaveIdWatcher; - /// Tracks changes to the location list. - private readonly ICollectionWatcher LocationListWatcher; - - /// Tracks changes to each location. - private readonly IDictionary LocationWatchers = new Dictionary(); + /// Tracks changes to the game's locations. + private readonly WorldLocationsTracker LocationsWatcher; /// Tracks changes to . private readonly IValueWatcher 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)Game1.locations); + this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)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 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 +{ + /// A watcher which detects changes to a Netcode collection. + internal class NetCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + where TValue : INetObject + { + /********* + ** Properties + *********/ + /// The field being watched. + private readonly NetCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs demoved since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetCollectionWatcher(NetCollection field) + { + this.Field = field; + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the collection. + /// The added value. + private void OnValueAdded(TValue value) + { + this.AddedImpl.Add(value); + } + + /// A callback invoked when an entry is removed from the collection. + /// The added value. + 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(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net collection. + public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : INetObject + { + return new NetCollectionWatcher(collection); + } + /// Get a watcher for a net dictionary. /// The dictionary key type. /// The dictionary value type. 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 /// The tracked location. public GameLocation Location { get; } - /// Tracks changes to the current location's objects. - public IDictionaryWatcher LocationObjectsWatcher { get; } + /// Tracks changes to the location's buildings. + public ICollectionWatcher BuildingsWatcher { get; } + + /// Tracks changes to the location's objects. + public IDictionaryWatcher 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)WatcherFactory.ForObservableCollection(new ObservableCollection()); + + 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 /// 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; } @@ -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> { @@ -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; } - /// 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. 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 +{ + /// Detects changes to the game's locations. + internal class WorldLocationsTracker : IWatcher + { + /********* + ** Properties + *********/ + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationListWatcher; + + /// A lookup of the tracked locations. + private IDictionary LocationDict { get; } = new Dictionary(); + + /// A lookup of registered buildings and their indoor location. + private readonly IDictionary BuildingIndoors = new Dictionary(); + + + /********* + ** 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(); + + /// The locations added since the last update. + public ICollection Removed { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game's list of locations. + public WorldLocationsTracker(ObservableCollection locations) + { + this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); + } + + /// Update the current value if needed. + 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 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(); + } + + /// Set the current location list as the baseline. + public void ResetLocationList() + { + this.Removed.Clear(); + this.Added.Clear(); + this.LocationListWatcher.Reset(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.ResetLocationList(); + foreach (IWatcher watcher in this.Locations) + watcher.Reset(); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + this.LocationListWatcher.Dispose(); + foreach (IWatcher watcher in this.Locations) + 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); + + // track change + this.Added.Add(location); + + // add + this.LocationDict[location] = new LocationTracker(location); + 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); + } + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 9f04887c..54fe9385 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,6 +85,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -142,12 +143,14 @@ + + @@ -175,8 +178,8 @@ - - + + -- cgit From 9d3e37317893126a9e0598f8f850abd23cad36c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 May 2018 16:14:41 -0400 Subject: update for Stardew Valley 1.3.11 (#521) --- .../Framework/StateTracking/FieldWatchers/WatcherFactory.cs | 9 +++++++++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework/StateTracking/PlayerTracker.cs') diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index a4982faa..4f1ac9f4 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -36,6 +36,15 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new ObservableCollectionWatcher(collection); } + /// Get a watcher for a net collection. + /// The value type. + /// The net field instance type. + /// The net collection. + public static NetValueWatcher ForNetValue(NetFieldBase field) where TSelf : NetFieldBase + { + return new NetValueWatcher(field); + } + /// Get a watcher for a net collection. /// The value type. /// The net collection. diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index dea2e30d..3814e534 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -59,12 +59,12 @@ namespace StardewModdingAPI.Framework.StateTracking 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) + [EventArgsLevelUp.LevelType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), + [EventArgsLevelUp.LevelType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), + [EventArgsLevelUp.LevelType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), + [EventArgsLevelUp.LevelType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), + [EventArgsLevelUp.LevelType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), + [EventArgsLevelUp.LevelType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) }; // track watchers for convenience -- cgit