From 9c9a0a41b041a1799904e78596fdf1d77451e1c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 5 Dec 2019 22:10:57 -0500 Subject: update for 'force off' gamepad option added in Stardew Valley 1.4.0.1 --- src/SMAPI/Framework/Input/SInputState.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index d69e5604..84cea36c 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -129,6 +129,9 @@ namespace StardewModdingAPI.Framework.Input [Obsolete("This method should only be called by the game itself.")] public override GamePadState GetGamePadState() { + if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff) + return base.GetGamePadState(); + return this.ShouldSuppressNow() ? this.SuppressedController : this.RealController; -- cgit From 238fbfe5698fb1791d47e8772ba1c5a86f9300ca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Dec 2019 12:20:59 -0500 Subject: let mods use Read/WriteSaveData while a save is being loaded --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/DataHelper.cs | 48 +++++++++++++++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8754e777..fc3cd4f7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ * For modders: * Added asset propagation for grass textures. + * `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event). * For the web UI: * If a JSON validator upload can't be saved to Pastebin (e.g. due to rate limits), it's now uploaded to Amazon S3 instead. Files uploaded to S3 expire after one month. diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index cc08c42b..3d43c539 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using Newtonsoft.Json; +using StardewModdingAPI.Enums; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -77,33 +79,45 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The player hasn't loaded a save file yet or isn't the main player. public TModel ReadSaveData(string key) where TModel : class { - if (!Game1.hasLoadedGame) + if (Context.LoadStage == LoadStage.None) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded."); if (!Game1.IsMasterGame) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); - return Game1.CustomData.TryGetValue(this.GetSaveFileKey(key), out string value) - ? this.JsonHelper.Deserialize(value) - : null; + + string internalKey = this.GetSaveFileKey(key); + foreach (IDictionary dataField in this.GetDataFields(Context.LoadStage)) + { + if (dataField.TryGetValue(internalKey, out string value)) + return this.JsonHelper.Deserialize(value); + } + return null; } /// Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The unique key identifying the data. - /// The arbitrary data to save. + /// The arbitrary data to save. /// The player hasn't loaded a save file yet or isn't the main player. - public void WriteSaveData(string key, TModel data) where TModel : class + public void WriteSaveData(string key, TModel model) where TModel : class { - if (!Game1.hasLoadedGame) + if (Context.LoadStage == LoadStage.None) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded."); if (!Game1.IsMasterGame) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); string internalKey = this.GetSaveFileKey(key); - if (data != null) - Game1.CustomData[internalKey] = this.JsonHelper.Serialize(data, Formatting.None); - else - Game1.CustomData.Remove(internalKey); + string data = model != null + ? this.JsonHelper.Serialize(model, Formatting.None) + : null; + + foreach (IDictionary dataField in this.GetDataFields(Context.LoadStage)) + { + if (data != null) + dataField[internalKey] = data; + else + dataField.Remove(internalKey); + } } /**** @@ -146,6 +160,18 @@ namespace StardewModdingAPI.Framework.ModHelpers return $"smapi/mod-data/{this.ModID}/{key}".ToLower(); } + /// Get the data fields to read/write for save data. + /// The current load stage. + private IEnumerable> GetDataFields(LoadStage stage) + { + if (stage == LoadStage.None) + yield break; + + yield return Game1.CustomData; + if (SaveGame.loaded != null) + yield return SaveGame.loaded.CustomData; + } + /// Get the absolute path for a global data file. /// The unique key identifying the data. private string GetGlobalDataPath(string key) -- cgit From e4a7ca5826ae0cb372aec529c4f21a53c98079da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Dec 2019 23:22:19 -0500 Subject: batch asset editor/loader changes --- docs/release-notes.md | 1 + .../Framework/Content/AssetInterceptorChange.cs | 91 ++++++++++++++++++++++ src/SMAPI/Framework/ContentCoordinator.cs | 82 ------------------- src/SMAPI/Framework/SCore.cs | 27 +------ src/SMAPI/Framework/SGame.cs | 50 +++++++++++- 5 files changed, 143 insertions(+), 108 deletions(-) create mode 100644 src/SMAPI/Framework/Content/AssetInterceptorChange.cs (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index fc3cd4f7..690c6442 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For players: * Updated for the 'Force Off' gamepad mode added in Stardew Valley 1.4.0.1. * Fixed compatibility issue with Arch Linux. + * Internal optimizations. * For modders: * Added asset propagation for grass textures. diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs new file mode 100644 index 00000000..498afe36 --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs @@ -0,0 +1,91 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Content +{ + /// A wrapper for and for internal cache invalidation. + internal class AssetInterceptorChange + { + /********* + ** Accessors + *********/ + /// The mod which registered the interceptor. + public IModMetadata Mod { get; } + + /// The interceptor instance. + public object Instance { get; } + + /// Whether the asset interceptor was added since the last tick. Mutually exclusive with . + public bool WasAdded { get; } + + /// Whether the asset interceptor was removed since the last tick. Mutually exclusive with . + public bool WasRemoved => this.WasAdded; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod registering the interceptor. + /// The interceptor. This must be an or instance. + /// Whether the asset interceptor was added since the last tick; else removed. + public AssetInterceptorChange(IModMetadata mod, object instance, bool wasAdded) + { + this.Mod = mod ?? throw new ArgumentNullException(nameof(mod)); + this.Instance = instance ?? throw new ArgumentNullException(nameof(instance)); + this.WasAdded = wasAdded; + + if (!(instance is IAssetEditor) && !(instance is IAssetLoader)) + throw new InvalidCastException($"The provided {nameof(instance)} value must be an {nameof(IAssetEditor)} or {nameof(IAssetLoader)} instance."); + } + + /// Get whether this instance can intercept the given asset. + /// Basic metadata about the asset being loaded. + public bool CanIntercept(IAssetInfo asset) + { + MethodInfo canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic); + if (canIntercept == null) + throw new InvalidOperationException($"SMAPI couldn't access the {nameof(AssetInterceptorChange)}.{nameof(this.CanInterceptImpl)} implementation."); + + return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset }); + } + + + /********* + ** Private methods + *********/ + /// Get whether this instance can intercept the given asset. + /// The asset type. + /// Basic metadata about the asset being loaded. + private bool CanInterceptImpl(IAssetInfo asset) + { + // check edit + if (this.Instance is IAssetEditor editor) + { + try + { + return editor.CanEdit(asset); + } + catch (Exception ex) + { + this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + // check load + if (this.Instance is IAssetLoader loader) + { + try + { + return loader.CanLoad(asset); + } + catch (Exception ex) + { + this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + return false; + } + } +} diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 08ebe6a5..97b54c5b 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; @@ -188,59 +187,6 @@ namespace StardewModdingAPI.Framework return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); } - /// Purge assets from the cache that match one of the interceptors. - /// The asset editors for which to purge matching assets. - /// The asset loaders for which to purge matching assets. - /// Returns the invalidated asset names. - public IEnumerable InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) - { - if (!editors.Any() && !loaders.Any()) - return new string[0]; - - // get CanEdit/Load methods - MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); - MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); - if (canEdit == null || canLoad == null) - throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen - - // invalidate matching keys - return this.InvalidateCache(asset => - { - // check loaders - MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); - foreach (IAssetLoader loader in loaders) - { - try - { - if ((bool)canLoadGeneric.Invoke(loader, new object[] { asset })) - return true; - } - catch (Exception ex) - { - this.GetModFor(loader).LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - // check editors - MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); - foreach (IAssetEditor editor in editors) - { - try - { - if ((bool)canEditGeneric.Invoke(editor, new object[] { asset })) - return true; - } - catch (Exception ex) - { - this.GetModFor(editor).LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - // asset not affected by a loader or editor - return false; - }); - } - /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. @@ -308,33 +254,5 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Remove(contentManager); } - - /// Get the mod which registered an asset loader. - /// The asset loader. - /// The given loader couldn't be matched to a mod. - private IModMetadata GetModFor(IAssetLoader loader) - { - foreach (var pair in this.Loaders) - { - if (pair.Value.Contains(loader)) - return pair.Key; - } - - throw new KeyNotFoundException("This loader isn't associated with a known mod."); - } - - /// Get the mod which registered an asset editor. - /// The asset editor. - /// The given editor couldn't be matched to a mod. - private IModMetadata GetModFor(IAssetEditor editor) - { - foreach (var pair in this.Editors) - { - if (pair.Value.Contains(editor)) - return pair.Key; - } - - throw new KeyNotFoundException("This editor isn't associated with a known mod."); - } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f1873391..fb3506b4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -842,34 +842,11 @@ namespace StardewModdingAPI.Framework { if (metadata.Mod.Helper.Content is ContentHelper helper) { - helper.ObservableAssetEditors.CollectionChanged += (sender, e) => - { - if (e.NewItems?.Count > 0) - { - this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); - this.ContentCore.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); - } - }; - helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => - { - if (e.NewItems?.Count > 0) - { - this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); - this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); - } - }; + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.GameInstance.OnAssetInterceptorsChanged(metadata, e.NewItems, e.OldItems); + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.GameInstance.OnAssetInterceptorsChanged(metadata, e.NewItems, e.OldItems); } } - // reset cache now if any editors or loaders were added during entry - IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); - IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); - if (editors.Any() || loaders.Any()) - { - this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); - this.ContentCore.InvalidateCacheFor(editors, loaders); - } - // unlock mod integrations this.ModRegistry.AreAllModsInitialized = true; } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 47261862..4774233e 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,6 +13,7 @@ using Microsoft.Xna.Framework.Graphics; using Netcode; using StardewModdingAPI.Enums; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; @@ -99,7 +101,7 @@ namespace StardewModdingAPI.Framework private WatcherCore Watchers; /// A snapshot of the current state. - private WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); + private readonly WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); /// Whether post-game-startup initialization has been performed. private bool IsInitialized; @@ -133,6 +135,9 @@ namespace StardewModdingAPI.Framework /// This property must be threadsafe, since it's accessed from a separate console input thread. public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); + /// Asset interceptors added or removed since the last tick. + private readonly List ReloadAssetInterceptorsQueue = new List(); + /********* ** Protected methods @@ -249,6 +254,24 @@ namespace StardewModdingAPI.Framework this.Events.ReturnedToTitle.RaiseEmpty(); } + /// A callback invoked when a mod adds or removes an asset interceptor. + /// The mod which added or removed interceptors. + /// The added interceptors. + /// The removed interceptors. + internal void OnAssetInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed) + { + if (added != null) + { + foreach (object instance in added) + this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, instance, wasAdded: true)); + } + if (removed != null) + { + foreach (object instance in removed) + this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, instance, wasAdded: false)); + } + } + /// Constructor a content manager to read XNB files. /// The service provider to use to locate services. /// The root directory to search for content. @@ -404,6 +427,31 @@ namespace StardewModdingAPI.Framework return; } + + /********* + ** Reload assets when interceptors are added/removed + *********/ + if (this.ReloadAssetInterceptorsQueue.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.Monitor.Log( + "changed: " + + string.Join(", ", + this.ReloadAssetInterceptorsQueue + .GroupBy(p => p.Mod) + .OrderBy(p => p.Key.DisplayName) + .Select(modGroup => + $"{modGroup.Key.DisplayName} (" + + string.Join(", ", modGroup.GroupBy(p => p.WasAdded).ToDictionary(p => p.Key, p => p.Count()).Select(p => $"{(p.Key ? "added" : "removed")} {p.Value}")) + + ")" + ) + ) + ); + + this.ContentCore.InvalidateCache(asset => this.ReloadAssetInterceptorsQueue.Any(p => p.CanIntercept(asset))); + this.ReloadAssetInterceptorsQueue.Clear(); + } + /********* ** Execute commands *********/ -- cgit From ff94a8149ed5a0f597500bfb2b1896bdb2f1fff3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Dec 2019 23:46:32 -0500 Subject: fix assets not being disposed when a content manager is disposed --- src/SMAPI/Framework/Content/ContentCache.cs | 2 +- src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 4178b663..c252b7b6 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Framework.Content /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns the removed keys (if any). - public IEnumerable Remove(Func predicate, bool dispose = false) + public IEnumerable Remove(Func predicate, bool dispose) { List removed = new List(); foreach (string key in this.Cache.Keys.ToArray()) diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 5283340e..93fd729b 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -200,7 +200,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return true; } return false; - }); + }, dispose); return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); } -- cgit From 3ba718749c258e48d83d7c2fe6b2dc08f165a29a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 10:35:08 -0500 Subject: don't keep a reference to uncached assets --- .../ContentManagers/BaseContentManager.cs | 12 ++++-- .../ContentManagers/GameContentManager.cs | 47 ++++++++++++---------- 2 files changed, 33 insertions(+), 26 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 93fd729b..4cfeeeba 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -258,20 +258,24 @@ namespace StardewModdingAPI.Framework.ContentManagers : base.ReadAsset(assetName, disposable => this.Disposables.Add(new WeakReference(disposable))); } - /// Inject an asset into the cache. + /// Add tracking data to an asset and add it to the cache. /// The type of asset to inject. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The asset value. /// The language code for which to inject the asset. - protected virtual void Inject(string assetName, T value, LanguageCode language) + /// Whether to save the asset to the asset cache. + protected virtual void TrackAsset(string assetName, T value, LanguageCode language, bool useCache) { // track asset key if (value is Texture2D texture) texture.Name = assetName; // cache asset - assetName = this.AssertAndNormalizeAssetName(assetName); - this.Cache[assetName] = value; + if (useCache) + { + assetName = this.AssertAndNormalizeAssetName(assetName); + this.Cache[assetName] = value; + } } /// Parse a cache key into its component parts. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 0b563555..04c4564f 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -83,8 +83,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { T managedAsset = this.Coordinator.LoadManagedAsset(contentManagerID, relativePath); - if (useCache) - this.Inject(assetName, managedAsset, language); + this.TrackAsset(assetName, managedAsset, language, useCache); return managedAsset; } @@ -111,7 +110,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // update cache & return data - this.Inject(assetName, data, language); + this.TrackAsset(assetName, data, language, useCache); return data; } @@ -169,18 +168,19 @@ namespace StardewModdingAPI.Framework.ContentManagers return false; } - /// Inject an asset into the cache. + /// Add tracking data to an asset and add it to the cache. /// The type of asset to inject. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The asset value. /// The language code for which to inject the asset. - protected override void Inject(string assetName, T value, LanguageCode language) + /// Whether to save the asset to the asset cache. + protected override void TrackAsset(string assetName, T value, LanguageCode language, bool useCache) { // handle explicit language in asset name { if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) { - this.Inject(newAssetName, value, newLanguage); + this.TrackAsset(newAssetName, value, newLanguage, useCache); return; } } @@ -192,24 +192,27 @@ namespace StardewModdingAPI.Framework.ContentManagers // only caches by the most specific key). // 2. Because a mod asset loader/editor may have changed the asset in a way that // doesn't change the instance stored in the cache, e.g. using `asset.ReplaceWith`. - string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; - base.Inject(assetName, value, language); - if (this.Cache.ContainsKey(keyWithLocale)) - base.Inject(keyWithLocale, value, language); - - // track whether the injected asset is translatable for is-loaded lookups - if (this.Cache.ContainsKey(keyWithLocale)) - { - this.IsLocalizableLookup[assetName] = true; - this.IsLocalizableLookup[keyWithLocale] = true; - } - else if (this.Cache.ContainsKey(assetName)) + if (useCache) { - this.IsLocalizableLookup[assetName] = false; - this.IsLocalizableLookup[keyWithLocale] = false; + string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; + base.TrackAsset(assetName, value, language, useCache: true); + if (this.Cache.ContainsKey(keyWithLocale)) + base.TrackAsset(keyWithLocale, value, language, useCache: true); + + // track whether the injected asset is translatable for is-loaded lookups + if (this.Cache.ContainsKey(keyWithLocale)) + { + this.IsLocalizableLookup[assetName] = true; + this.IsLocalizableLookup[keyWithLocale] = true; + } + else if (this.Cache.ContainsKey(assetName)) + { + this.IsLocalizableLookup[assetName] = false; + this.IsLocalizableLookup[keyWithLocale] = false; + } + else + this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); } - else - this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); } /// Load an asset file directly from the underlying content manager. -- cgit From 6dc442803fe4fbe2a38b9fb287990cc8692c17eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 10:38:17 -0500 Subject: fix private assets from content packs not having tracking info --- docs/release-notes.md | 1 + .../Framework/ContentManagers/ModContentManager.cs | 30 +++++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 690c6442..6f06d3d2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added asset propagation for grass textures. * `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event). + * Fixed private textures loaded from content packs not having their `Name` field set. * For the web UI: * If a JSON validator upload can't be saved to Pastebin (e.g. due to rate limits), it's now uploaded to Amazon S3 instead. Files uploaded to S3 expire after one month. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 90b86179..fdf76b24 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -105,6 +105,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get local asset SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}"); + T asset; try { // get file @@ -118,22 +119,22 @@ namespace StardewModdingAPI.Framework.ContentManagers // XNB file case ".xnb": { - T data = this.RawLoad(assetName, useCache: false); - if (data is Map map) + asset = this.RawLoad(assetName, useCache: false); + if (asset is Map map) { this.NormalizeTilesheetPaths(map); this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); } - return data; } + break; // unpacked data case ".json": { - if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T data)) + if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out asset)) throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above - return data; } + break; // unpacked image case ".png": @@ -143,13 +144,13 @@ namespace StardewModdingAPI.Framework.ContentManagers throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - return (T)(object)texture; - } + using FileStream stream = File.OpenRead(file.FullName); + + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + asset = (T)(object)texture; } + break; // unpacked map case ".tbin": @@ -163,8 +164,9 @@ namespace StardewModdingAPI.Framework.ContentManagers Map map = formatManager.LoadMap(file.FullName); this.NormalizeTilesheetPaths(map); this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); - return (T)(object)map; + asset = (T)(object)map; } + break; default: throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'."); @@ -176,6 +178,10 @@ namespace StardewModdingAPI.Framework.ContentManagers throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex); } + + // track & return asset + this.TrackAsset(assetName, asset, language, useCache); + return asset; } /// Create a new content manager for temporary use. -- cgit From 16f986c51b9c87c2253a39fd771dcc24f7c43db4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 21:31:34 -0500 Subject: refactor cache invalidation & propagation to allow for future optimizations --- src/SMAPI/Framework/Content/ContentCache.cs | 5 +- src/SMAPI/Framework/ContentCoordinator.cs | 25 ++++---- .../ContentManagers/BaseContentManager.cs | 16 +++--- .../ContentManagers/GameContentManager.cs | 2 +- .../Framework/ContentManagers/IContentManager.cs | 4 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 67 ++++++++++++---------- 6 files changed, 65 insertions(+), 54 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index c252b7b6..f33ff84d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -119,13 +119,12 @@ namespace StardewModdingAPI.Framework.Content /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns the removed keys (if any). - public IEnumerable Remove(Func predicate, bool dispose) + public IEnumerable Remove(Func predicate, bool dispose) { List removed = new List(); foreach (string key in this.Cache.Keys.ToArray()) { - Type type = this.Cache[key].GetType(); - if (predicate(key, type)) + if (predicate(key, this.Cache[key])) { this.Remove(key, dispose); removed.Add(key); diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 97b54c5b..82d3805b 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; @@ -207,24 +208,28 @@ namespace StardewModdingAPI.Framework /// Returns the invalidated asset names. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { - // invalidate cache - IDictionary removedAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + // invalidate cache & track removed assets + IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); foreach (IContentManager contentManager in this.ContentManagers) { - foreach (Tuple asset in contentManager.InvalidateCache(predicate, dispose)) - removedAssetNames[asset.Item1] = asset.Item2; + foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + { + if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) + removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); + assets.Add(entry.Value); + } } // reload core game assets - int reloaded = this.CoreAssets.Propagate(this.MainContentManager, removedAssetNames); // use an intercepted content manager - - // report result - if (removedAssetNames.Any()) - this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + if (removedAssets.Any()) + { + IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager + this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); + } else this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return removedAssetNames.Keys; + return removedAssets.Keys; } /// Dispose held resources. diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 4cfeeeba..41ce7c37 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -184,25 +184,25 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns the invalidated asset names and types. - public IEnumerable> InvalidateCache(Func predicate, bool dispose = false) + /// Returns the invalidated asset names and instances. + public IDictionary InvalidateCache(Func predicate, bool dispose = false) { - Dictionary removeAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => + IDictionary removeAssets = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, asset) => { this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.ContainsKey(assetName)) + if (removeAssets.ContainsKey(assetName)) return true; - if (predicate(assetName, type)) + if (predicate(assetName, asset.GetType())) { - removeAssetNames[assetName] = type; + removeAssets[assetName] = asset; return true; } return false; }, dispose); - return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); + return removeAssets; } /// Dispose held resources. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 04c4564f..8930267d 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Framework.ContentManagers removeAssetNames.Contains(key) || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) ) - .Select(p => p.Item1) + .Select(p => p.Key) .OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase) .ToArray(); if (invalidated.Any()) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 12c01352..8da9a777 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns the invalidated asset names and types. - IEnumerable> InvalidateCache(Func predicate, bool dispose = false); + /// Returns the invalidated asset names and instances. + IDictionary InvalidateCache(Func predicate, bool dispose = false); } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 8b00d893..84102828 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -65,8 +65,8 @@ namespace StardewModdingAPI.Metadata /// Reload one of the game's core assets (if applicable). /// The content manager through which to reload the asset. /// The asset keys and types to reload. - /// Returns the number of reloaded assets. - public int Propagate(LocalizedContentManager content, IDictionary assets) + /// Returns a lookup of asset names to whether they've been propagated. + public IDictionary Propagate(LocalizedContentManager content, IDictionary assets) { // group into optimized lists var buckets = assets.GroupBy(p => @@ -81,25 +81,26 @@ namespace StardewModdingAPI.Metadata }); // reload assets - int reloaded = 0; + IDictionary propagated = assets.ToDictionary(p => p.Key, p => false, StringComparer.InvariantCultureIgnoreCase); foreach (var bucket in buckets) { switch (bucket.Key) { case AssetBucket.Sprite: - reloaded += this.ReloadNpcSprites(content, bucket.Select(p => p.Key)); + this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated); break; case AssetBucket.Portrait: - reloaded += this.ReloadNpcPortraits(content, bucket.Select(p => p.Key)); + this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated); break; default: - reloaded += bucket.Count(p => this.PropagateOther(content, p.Key, p.Value)); + foreach (var entry in bucket) + propagated[entry.Key] = this.PropagateOther(content, entry.Key, entry.Value); break; } } - return reloaded; + return propagated; } @@ -750,51 +751,57 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for matching NPCs. /// The content manager through which to reload the asset. /// The asset keys to reload. - /// Returns the number of reloaded assets. - private int ReloadNpcSprites(LocalizedContentManager content, IEnumerable keys) + /// The asset keys which have been propagated. + private void ReloadNpcSprites(LocalizedContentManager content, IEnumerable keys, IDictionary propagated) { // get NPCs HashSet lookup = new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); - NPC[] characters = this.GetCharacters() - .Where(npc => npc.Sprite != null && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name))) + var characters = + ( + from npc in this.GetCharacters() + let key = this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name) + where key != null && lookup.Contains(key) + select new { Npc = npc, Key = key } + ) .ToArray(); if (!characters.Any()) - return 0; + return; // update sprite - int reloaded = 0; - foreach (NPC npc in characters) + foreach (var target in characters) { - this.SetSpriteTexture(npc.Sprite, content.Load(npc.Sprite.textureName.Value)); - reloaded++; + this.SetSpriteTexture(target.Npc.Sprite, content.Load(target.Key)); + propagated[target.Key] = true; } - - return reloaded; } /// Reload the portraits for matching NPCs. /// The content manager through which to reload the asset. /// The asset key to reload. - /// Returns the number of reloaded assets. - private int ReloadNpcPortraits(LocalizedContentManager content, IEnumerable keys) + /// The asset keys which have been propagated. + private void ReloadNpcPortraits(LocalizedContentManager content, IEnumerable keys, IDictionary propagated) { // get NPCs HashSet lookup = new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); - var villagers = this - .GetCharacters() - .Where(npc => npc.isVillager() && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name))) + var characters = + ( + from npc in this.GetCharacters() + where npc.isVillager() + + let key = this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name) + where key != null && lookup.Contains(key) + select new { Npc = npc, Key = key } + ) .ToArray(); - if (!villagers.Any()) - return 0; + if (!characters.Any()) + return; // update portrait - int reloaded = 0; - foreach (NPC npc in villagers) + foreach (var target in characters) { - npc.Portrait = content.Load(npc.Portrait.Name); - reloaded++; + target.Npc.Portrait = content.Load(target.Key); + propagated[target.Key] = true; } - return reloaded; } /// Reload tree textures. -- cgit From d662ea858c4914eefe5a0b0f911d1f41086b0424 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 15 Dec 2019 00:33:08 -0500 Subject: improve error message for TargetParameterCountException in the reflection API --- docs/release-notes.md | 1 + src/SMAPI/Framework/Reflection/ReflectedMethod.cs | 4 ++++ 2 files changed, 5 insertions(+) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0185d1ec..dc38710a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Added asset propagation for grass textures. * Added asset propagation for `Data\Bundles` changes (for added bundles only). + * Improved error messages for `TargetParameterCountException` when using the reflection API. * `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event). * Fixed private textures loaded from content packs not having their `Name` field set. diff --git a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs index 039f27c3..82737a7f 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs @@ -65,6 +65,10 @@ namespace StardewModdingAPI.Framework.Reflection { result = this.MethodInfo.Invoke(this.Parent, arguments); } + catch (TargetParameterCountException) + { + throw new Exception($"Couldn't invoke the {this.DisplayName} method: it expects {this.MethodInfo.GetParameters().Length} parameters, but {arguments.Length} were provided."); + } catch (Exception ex) { throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); -- cgit From 6275821288aec6a5178f660eda951e6343f5e381 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 15 Dec 2019 01:08:35 -0500 Subject: add friendly log message for save file-not-found errors --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 59 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 9 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index dc38710a..217b0f34 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming release * For players: + * Added friendly log message for save file-not-found errors. * Updated for the 'Force Off' gamepad mode added in Stardew Valley 1.4.1. * Fixed compatibility with Linux Mint 18 (thanks to techge!) and Arch Linux. * Internal optimizations. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index fb3506b4..2c6c0e76 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -97,16 +97,25 @@ namespace StardewModdingAPI.Framework }; /// Regex patterns which match console messages to show a more friendly error for. - private readonly Tuple[] ReplaceConsolePatterns = + private readonly ReplaceLogPattern[] ReplaceConsolePatterns = { - Tuple.Create( - new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.", RegexOptions.Compiled | RegexOptions.CultureInvariant), + // Steam not loaded + new ReplaceLogPattern( + search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + replacement: #if SMAPI_FOR_WINDOWS - "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).", + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).", #else - "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", #endif - LogLevel.Error + logLevel: LogLevel.Error + ), + + // save file not found error + new ReplaceLogPattern( + search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.", + logLevel: LogLevel.Error ) }; @@ -1294,11 +1303,12 @@ namespace StardewModdingAPI.Framework return; // show friendly error if applicable - foreach (var entry in this.ReplaceConsolePatterns) + foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns) { - if (entry.Item1.IsMatch(message)) + string newMessage = entry.Search.Replace(message, entry.Replacement); + if (message != newMessage) { - this.Monitor.Log(entry.Item2, entry.Item3); + gameMonitor.Log(newMessage, entry.LogLevel); gameMonitor.Log(message, LogLevel.Trace); return; } @@ -1388,5 +1398,36 @@ namespace StardewModdingAPI.Framework } } } + + /// A console log pattern to replace with a different message. + private class ReplaceLogPattern + { + /********* + ** Accessors + *********/ + /// The regex pattern matching the portion of the message to replace. + public Regex Search { get; } + + /// The replacement string. + public string Replacement { get; } + + /// The log level for the new message. + public LogLevel LogLevel { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The regex pattern matching the portion of the message to replace. + /// The replacement string. + /// The log level for the new message. + public ReplaceLogPattern(Regex search, string replacement, LogLevel logLevel) + { + this.Search = search; + this.Replacement = replacement; + this.LogLevel = logLevel; + } + } } } -- cgit From 0a00c70397d85777499dcf7877cef08727a2dbae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 20 Dec 2019 20:27:21 -0500 Subject: add console warning in paranoid mode --- docs/release-notes.md | 1 + src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs | 7 +++++-- src/SMAPI/Framework/Content/AssetDataForImage.cs | 4 ++-- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 5 +++++ src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 4 ++++ src/SMAPI/Metadata/InstructionMetadata.cs | 1 + 7 files changed, 21 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 772d95aa..761012df 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ * For modders: * Added asset propagation for grass textures. * Added asset propagation for `Data\Bundles` changes (for added bundles only). + * Added direct `Console` access to paranoid mode warnings. * Improved error messages for `TargetParameterCountException` when using the reflection API. * `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event). * Fixed private textures loaded from content packs not having their `Name` field set. diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs index e67616d0..925e0b5c 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs @@ -27,10 +27,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The mod has no update keys set. NoUpdateKeys = 32, + /// Uses .NET APIs for reading and writing to the console. + AccessesConsole = 64, + /// Uses .NET APIs for filesystem access. - AccessesFilesystem = 64, + AccessesFilesystem = 128, /// Uses .NET APIs for shell or process access. - AccessesShell = 128 + AccessesShell = 256 } } diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 4ae2ad68..aa615a0b 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -42,8 +42,8 @@ namespace StardewModdingAPI.Framework.Content Texture2D target = this.Data; // get areas - sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); - targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + sourceArea ??= new Rectangle(0, 0, source.Width, source.Height); + targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 7670eb3a..b5533335 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -356,6 +356,11 @@ namespace StardewModdingAPI.Framework.ModLoading mod.SetWarning(ModWarning.UsesDynamic); break; + case InstructionHandleResult.DetectedConsoleAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected direct console access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesConsole); + break; + case InstructionHandleResult.DetectedFilesystemAccess: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.AccessesFilesystem); diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index d93b603d..a948213b 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The instruction is compatible, but references or which may impact stability. DetectedUnvalidatedUpdateTick, + /// The instruction accesses the SMAPI console directly. + DetectedConsoleAccess, + /// The instruction accesses the filesystem directly. DetectedFilesystemAccess, diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2c6c0e76..c9c9d14a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1102,6 +1102,10 @@ namespace StardewModdingAPI.Framework ); if (this.Settings.ParanoidWarnings) { + LogWarningGroup(ModWarning.AccessesConsole, LogLevel.Warn, "Accesses the console directly", + "These mods directly access the SMAPI console, and you enabled paranoid warnings. (Note that this may be", + "legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly", "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be", "legitimate and innocent usage; this warning is meaningless without further investigation.)" diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 95482708..eee5c235 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -60,6 +60,7 @@ namespace StardewModdingAPI.Metadata if (paranoidMode) { // filesystem access + yield return new TypeFinder(typeof(System.Console).FullName, InstructionHandleResult.DetectedConsoleAccess); yield return new TypeFinder(typeof(System.IO.File).FullName, InstructionHandleResult.DetectedFilesystemAccess); yield return new TypeFinder(typeof(System.IO.FileStream).FullName, InstructionHandleResult.DetectedFilesystemAccess); yield return new TypeFinder(typeof(System.IO.FileInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); -- cgit From d932a11f51392cd42ab501185982af971f954c8d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 24 Dec 2019 13:51:21 -0500 Subject: list broken dependencies first in 'skipped mods' list --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 4 +++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 21 ++++++++++++ src/SMAPI/Framework/SCore.cs | 48 +++++++++++++++++++-------- 4 files changed, 61 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index dc0f584d..d31b95e5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For players: * Added friendly log message for save file-not-found errors. * Updated for the 'Force Off' gamepad mode added in Stardew Valley 1.4.1. + * The 'skipped mods' list now shows broken dependencies first, so it's easier to see which ones to fix first. * Fixed compatibility with Linux Mint 18 (thanks to techge!) and Arch Linux. * Fixed compatibility with Linux systems which have libhybris-utils installed. * Fixes for the bundled Console Commands mod: diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 6ee7df69..37927482 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -105,6 +105,10 @@ namespace StardewModdingAPI.Framework /// Only return valid update keys. IEnumerable GetUpdateKeys(bool validOnly = true); + /// Get the mod IDs that must be installed to load this mod. + /// Whether to include optional dependencies. + IEnumerable GetRequiredModIds(bool includeOptional = false); + /// Whether the mod has at least one valid update key set. bool HasValidUpdateKeys(); diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 7f788d17..0e90362e 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -188,6 +188,27 @@ namespace StardewModdingAPI.Framework.ModLoading } } + /// Get the mod IDs that must be installed to load this mod. + /// Whether to include optional dependencies. + public IEnumerable GetRequiredModIds(bool includeOptional = false) + { + HashSet required = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + // yield dependencies + if (this.Manifest?.Dependencies != null) + { + foreach (var entry in this.Manifest?.Dependencies) + { + if ((entry.IsRequired || includeOptional) && required.Add(entry.UniqueID)) + yield return entry.UniqueID; + } + } + + // yield content pack parent + if (this.Manifest?.ContentPackFor?.UniqueID != null && required.Add(this.Manifest.ContentPackFor.UniqueID)) + yield return this.Manifest.ContentPackFor.UniqueID; + } + /// Whether the mod has at least one valid update key set. public bool HasValidUpdateKeys() { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c9c9d14a..a89c14d7 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1046,26 +1046,48 @@ namespace StardewModdingAPI.Framework // log skipped mods if (skippedMods.Any()) { + // get logging logic + HashSet logged = new HashSet(); + void LogSkippedMod(IModMetadata mod, string errorReason, string errorDetails) + { + string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}"; + + if (logged.Add($"{message}|{errorDetails}")) + { + this.Monitor.Log(message, LogLevel.Error); + if (errorDetails != null) + this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); + } + } + + // find skipped dependencies + KeyValuePair>[] skippedDependencies; + { + HashSet skippedDependencyIds = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet skippedModIds = new HashSet(from mod in skippedMods where mod.Key.HasID() select mod.Key.Manifest.UniqueID, StringComparer.InvariantCultureIgnoreCase); + foreach (IModMetadata mod in skippedMods.Keys) + { + foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds())) + skippedDependencyIds.Add(requiredId); + } + skippedDependencies = skippedMods.Where(p => p.Key.HasID() && skippedDependencyIds.Contains(p.Key.Manifest.UniqueID)).ToArray(); + } + + // log skipped mods this.Monitor.Log(" Skipped mods", LogLevel.Error); this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); this.Monitor.Newline(); - HashSet logged = new HashSet(); - foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) + if (skippedDependencies.Any()) { - IModMetadata mod = pair.Key; - string errorReason = pair.Value.Item1; - string errorDetails = pair.Value.Item2; - string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}"; - - if (!logged.Add($"{message}|{errorDetails}")) - continue; // skip duplicate messages (e.g. if multiple copies of the mod are installed) - - this.Monitor.Log(message, LogLevel.Error); - if (errorDetails != null) - this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); + foreach (var pair in skippedDependencies.OrderBy(p => p.Key.DisplayName)) + LogSkippedMod(pair.Key, pair.Value.Item1, pair.Value.Item2); + this.Monitor.Newline(); } + + foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) + LogSkippedMod(pair.Key, pair.Value.Item1, pair.Value.Item2); this.Monitor.Newline(); } -- cgit From 360a982336e8b250d79597e323ba267c70c404c2 Mon Sep 17 00:00:00 2001 From: Dan Volchek Date: Thu, 26 Dec 2019 20:34:39 -0800 Subject: fix log parser content pack list entry pattern, remove unneeded ternary in content pack logging --- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- src/SMAPI/Framework/SCore.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 66a3687f..1210f708 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's content pack list. - private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+) (?.+) by (?.+) \| for (?.+?)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?[^\|]+))? \| for (?[^\|]+)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's mod update list. private readonly Regex ModUpdateListStartPattern = new Regex(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a89c14d7..6e7c8fcb 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -783,7 +783,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log( $" {metadata.DisplayName} {manifest.Version}" + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), LogLevel.Info ); -- cgit From d9a9cef172be8c34b57e345732bf4e582898bcbc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 Dec 2019 11:27:25 -0500 Subject: fix ObservableCollection watcher not handling list reset (#685) --- .../FieldWatchers/ObservableCollectionWatcher.cs | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs index 883b1023..c29d2783 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs @@ -21,6 +21,9 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// The pairs removed since the last reset. private readonly List RemovedImpl = new List(); + /// The previous values as of the last update. + private readonly List PreviousValues = new List(); + /********* ** Accessors @@ -78,10 +81,27 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// The event arguments. private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - if (e.NewItems != null) - this.AddedImpl.AddRange(e.NewItems.Cast()); - if (e.OldItems != null) - this.RemovedImpl.AddRange(e.OldItems.Cast()); + if (e.Action == NotifyCollectionChangedAction.Reset) + { + this.RemovedImpl.AddRange(this.PreviousValues); + this.PreviousValues.Clear(); + } + else + { + TValue[] added = e.NewItems?.Cast().ToArray(); + TValue[] removed = e.OldItems?.Cast().ToArray(); + + if (removed != null) + { + this.RemovedImpl.AddRange(removed); + this.PreviousValues.RemoveRange(e.OldStartingIndex, removed.Length); + } + if (added != null) + { + this.AddedImpl.AddRange(added); + this.PreviousValues.InsertRange(e.NewStartingIndex, added); + } + } } } } -- cgit From dca60f42b2048d6b0b27517b9e7686665e61e9c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 Dec 2019 16:18:11 -0500 Subject: fix XNA keeping loaded assets alive forever (#685) --- docs/release-notes.md | 2 ++ src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 8 ++++++++ 2 files changed, 10 insertions(+) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ce79d9b..9ac95d86 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,8 @@ * The 'skipped mods' list now shows broken dependencies first, so it's easier to see which ones to fix first. * Fixed compatibility with Linux Mint 18 (thanks to techge!) and Arch Linux. * Fixed compatibility with Linux systems which have libhybris-utils installed. + * Fixed memory leak when repeatedly loading a save and returning to title. + * Fixed memory leak when mods reload assets. * Fixes for the bundled Console Commands mod: * added new clothing items; * fixed spawning new flooring and rings (thanks to Mizzion!); diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 41ce7c37..36f2f650 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -41,6 +41,10 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A list of disposable assets. private readonly List> Disposables = new List>(); + /// The disposable assets tracked by the base content manager. + /// This should be kept empty to avoid keeping disposable assets referenced forever, which prevents garbage collection when they're unused. Disposable assets are tracked by instead, which avoids a hard reference. + private readonly List BaseDisposableReferences; + /********* ** Accessors @@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get asset data this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + this.BaseDisposableReferences = reflection.GetField>(this, "disposableAssets").GetValue(); } /// Load an asset that has been processed by the content pipeline. @@ -276,6 +281,9 @@ namespace StardewModdingAPI.Framework.ContentManagers assetName = this.AssertAndNormalizeAssetName(assetName); this.Cache[assetName] = value; } + + // avoid hard disposable references; see remarks on the field + this.BaseDisposableReferences.Clear(); } /// Parse a cache key into its component parts. -- cgit From aef1b8ac2898e147e6200fe257e8fdd82ee7fdbc Mon Sep 17 00:00:00 2001 From: wartech0 Date: Sun, 29 Dec 2019 08:06:02 -0600 Subject: Added the new ChestItemChanged event. --- src/SMAPI.sln | 4 +- src/SMAPI/Events/ChestItemChangedEventArgs.cs | 47 ++++++++++ src/SMAPI/Events/IWorldEvents.cs | 3 + src/SMAPI/Framework/Events/EventManager.cs | 4 + src/SMAPI/Framework/Events/ModWorldEvents.cs | 6 ++ src/SMAPI/Framework/SGame.cs | 4 + src/SMAPI/Framework/SnapshotListDiff.cs | 8 ++ .../StateTracking/FieldWatchers/NetListWatcher.cs | 100 +++++++++++++++++++++ .../Framework/StateTracking/LocationTracker.cs | 21 +++++ .../StateTracking/Snapshots/LocationSnapshot.cs | 5 ++ 10 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Events/ChestItemChangedEventArgs.cs create mode 100644 src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 62eaa777..65ea330c 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28729.10 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.902 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject diff --git a/src/SMAPI/Events/ChestItemChangedEventArgs.cs b/src/SMAPI/Events/ChestItemChangedEventArgs.cs new file mode 100644 index 00000000..6b06487c --- /dev/null +++ b/src/SMAPI/Events/ChestItemChangedEventArgs.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Item = StardewValley.Item; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class ChestItemChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable Added { get; } + + /// The objects removed from the location. + public IEnumerable Removed { get; } + + /// The location of the chest from where the item was added or removed + public Vector2 LocationOfChest { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + internal ChestItemChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed, Vector2 locationOfChest) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + this.LocationOfChest = locationOfChest; + } + } +} diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 0ceffcc1..6f9b71a7 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Events /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; + /// Raised after items are added or removed from a chest in a location. + event EventHandler ChestItemChanged; + /// Raised after terrain features (like floors and trees) are added or removed in a location. event EventHandler TerrainFeatureListChanged; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 18b00f69..dad45f95 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -148,6 +148,9 @@ namespace StardewModdingAPI.Framework.Events /// Raised after objects are added or removed in a location. public readonly ManagedEvent ObjectListChanged; + /// Raised after a new item is added or removed from a chest in a location. + public readonly ManagedEvent ChestItemsChanged; + /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent TerrainFeatureListChanged; @@ -221,6 +224,7 @@ namespace StardewModdingAPI.Framework.Events this.LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.ChestItemsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ChestItemChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index b85002a3..8d6701ec 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -51,6 +51,12 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ObjectListChanged.Remove(value); } + public event EventHandler ChestItemChanged + { + add => this.EventManager.ChestItemsChanged.Add(value); + remove => this.EventManager.ChestItemsChanged.Remove(value); + } + /// Raised after terrain features (like floors and trees) are added or removed in a location. public event EventHandler TerrainFeatureListChanged { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4774233e..564fec1d 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -702,6 +702,10 @@ namespace StardewModdingAPI.Framework if (locState.Objects.IsChanged) events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); + // chest items changed + if (locState.ChestItems.IsChanged) + events.ChestItemsChanged.Raise(new ChestItemChangedEventArgs(location, locState.ChestItems.Added, locState.ChestItems.Removed, locState.ChestItems.Key)); + // terrain features changed if (locState.TerrainFeatures.IsChanged) events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed)); diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs index d4d5df50..b14cacdb 100644 --- a/src/SMAPI/Framework/SnapshotListDiff.cs +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -29,10 +29,18 @@ namespace StardewModdingAPI.Framework /// The added values. public IEnumerable Added => this.AddedImpl; + public Microsoft.Xna.Framework.Vector2 Key; /********* ** Public methods *********/ + + public void Update(ICollectionWatcher watcher, Microsoft.Xna.Framework.Vector2 key) + { + this.Key = key; + this.Update(watcher.IsChanged, watcher.Removed, watcher.Added); + } + /// Update the snapshot. /// Whether the value changed since the last update. /// The removed values. diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs new file mode 100644 index 00000000..cac66356 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + internal class NetListWatcher : BaseDisposableWatcher, ICollectionWatcher + where TValue : class, INetObject + { + + + /********* + ** Fields + *********/ + /// The field being watched. + private readonly NetList> Field; + + public TKey Key { get; } + + /// The pairs added since the last reset. + private readonly IList AddedImpl = new List(); + + /// The pairs removed since the last reset. + private readonly IList RemovedImpl = new List(); + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetListWatcher(NetList> field, TKey key) + { + this.Field = field; + this.Key = key; + field.OnElementChanged += this.OnElementChanged; + field.OnArrayReplaced += this.OnArrayReplaced; + } + + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + public IEnumerable Added => this.AddedImpl; + + public IEnumerable Removed => this.RemovedImpl; + + public void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnElementChanged -= this.OnElementChanged; + this.Field.OnArrayReplaced -= this.OnArrayReplaced; + } + + base.Dispose(); + } + + public void Reset() + { + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + public void Update() + { + this.AssertNotDisposed(); + } + + /********* + ** Private methods + *********/ + private void OnArrayReplaced(NetList> list, IList before, IList after) + { + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + + foreach(var obj in after) + this.AddedImpl.Add(obj); + + foreach(var obj in before) + this.RemovedImpl.Add(obj); + } + + private void OnElementChanged(NetList> list, int index, TValue oldValue, TValue newValue) + { + + /* checks for stack addition / subtraction changing stacks does not fire off an element changed event + if ((oldValue != null && newValue != null) && oldValue.CompareTo(newValue) < 0) + this.AddedImpl.Add(newValue); + //Stack Removed from + if ((oldValue != null && newValue != null) && oldValue.CompareTo(newValue) > 0) + this.RemovedImpl.Add(newValue); + */ + + if(newValue!=null) + this.AddedImpl.Add(newValue); + + if(oldValue!=null) + this.RemovedImpl.Add(oldValue); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 1f479e12..ef4a8b64 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -7,6 +7,7 @@ using StardewValley.Buildings; using StardewValley.Locations; using StardewValley.TerrainFeatures; using Object = StardewValley.Object; +using Chest = StardewValley.Objects.Chest; namespace StardewModdingAPI.Framework.StateTracking { @@ -47,6 +48,7 @@ namespace StardewModdingAPI.Framework.StateTracking /// Tracks added or removed terrain features. public IDictionaryWatcher TerrainFeaturesWatcher { get; } + public Dictionary> activeChestWatchers = new Dictionary>(); /********* ** Public methods @@ -88,6 +90,25 @@ namespace StardewModdingAPI.Framework.StateTracking { foreach (IWatcher watcher in this.Watchers) watcher.Update(); + + foreach (KeyValuePair obj in this.ObjectsWatcher.Added.Where(p => p.Value is Chest)) + { + if (!this.activeChestWatchers.ContainsKey(obj.Key)) + { + //create a new watcher for chests items + Chest temp = obj.Value as Chest; + NetListWatcher tempItemWatcher = new NetListWatcher(temp.items, obj.Key); + this.Watchers.Add(tempItemWatcher); + this.activeChestWatchers.Add(obj.Key, tempItemWatcher); + } + } + + foreach (KeyValuePair obj in this.ObjectsWatcher.Removed) + { + this.activeChestWatchers.TryGetValue(obj.Key, out NetListWatcher tempItemWatcher); + this.Watchers.Remove(tempItemWatcher); + this.activeChestWatchers.Remove(obj.Key); + } } /// Set the current value as the baseline. diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index d3029540..31cf29c3 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -33,6 +33,8 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots /// Tracks added or removed terrain features. public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); + public SnapshotListDiff ChestItems { get; } = new SnapshotListDiff(); + /********* ** Public methods @@ -54,6 +56,9 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.Npcs.Update(watcher.NpcsWatcher); this.Objects.Update(watcher.ObjectsWatcher); this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher); + + foreach (var obj in watcher.activeChestWatchers) + this.ChestItems.Update(obj.Value, obj.Key); } } } -- cgit From 1286a90ec2fb0dcf26bd59feec714544844e4398 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Dec 2019 13:29:25 -0500 Subject: minor refactoring This commit... - removes key fields added to non-keyed types like NetListWatcher and SnapshotListDiff; - fixes existing chests not being watched; - fixes diffs not correctly updated for added/removed chests; - performs minor cleanup, adds missing docs, etc. --- src/SMAPI.sln | 4 +- src/SMAPI/Events/ChestInventoryChangedEventArgs.cs | 47 +++++++++++ src/SMAPI/Events/ChestItemChangedEventArgs.cs | 47 ----------- src/SMAPI/Events/IWorldEvents.cs | 4 +- src/SMAPI/Framework/Events/EventManager.cs | 6 +- src/SMAPI/Framework/Events/ModWorldEvents.cs | 7 +- src/SMAPI/Framework/SGame.cs | 11 ++- src/SMAPI/Framework/SnapshotListDiff.cs | 8 -- .../StateTracking/FieldWatchers/NetListWatcher.cs | 90 ++++++++++++---------- .../Framework/StateTracking/LocationTracker.cs | 74 +++++++++++------- .../StateTracking/Snapshots/LocationSnapshot.cs | 20 ++++- 11 files changed, 179 insertions(+), 139 deletions(-) create mode 100644 src/SMAPI/Events/ChestInventoryChangedEventArgs.cs delete mode 100644 src/SMAPI/Events/ChestItemChangedEventArgs.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 65ea330c..62eaa777 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.902 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28729.10 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs new file mode 100644 index 00000000..0b54e909 --- /dev/null +++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class ChestInventoryChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location containing the chest. + public GameLocation Location { get; } + + /// The tile position of the chest. + public Vector2 Tile { get; } + + /// The objects added to the location. + public IEnumerable Added { get; } + + /// The objects removed from the location. + public IEnumerable Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location containing the chest. + /// The tile position of the chest. + /// The objects added to the location. + /// The objects removed from the location. + internal ChestInventoryChangedEventArgs(GameLocation location, Vector2 tile, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Tile = tile; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/SMAPI/Events/ChestItemChangedEventArgs.cs b/src/SMAPI/Events/ChestItemChangedEventArgs.cs deleted file mode 100644 index 6b06487c..00000000 --- a/src/SMAPI/Events/ChestItemChangedEventArgs.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using StardewValley; -using Item = StardewValley.Item; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class ChestItemChangedEventArgs : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The objects added to the location. - public IEnumerable Added { get; } - - /// The objects removed from the location. - public IEnumerable Removed { get; } - - /// The location of the chest from where the item was added or removed - public Vector2 LocationOfChest { get; } - - /// Whether this is the location containing the local player. - public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The objects added to the location. - /// The objects removed from the location. - internal ChestItemChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed, Vector2 locationOfChest) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - this.LocationOfChest = locationOfChest; - } - } -} diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs index 6f9b71a7..9569a57b 100644 --- a/src/SMAPI/Events/IWorldEvents.cs +++ b/src/SMAPI/Events/IWorldEvents.cs @@ -23,8 +23,8 @@ namespace StardewModdingAPI.Events /// Raised after objects are added or removed in a location. event EventHandler ObjectListChanged; - /// Raised after items are added or removed from a chest in a location. - event EventHandler ChestItemChanged; + /// Raised after items are added or removed from a chest. + event EventHandler ChestInventoryChanged; /// Raised after terrain features (like floors and trees) are added or removed in a location. event EventHandler TerrainFeatureListChanged; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index dad45f95..892cbc7b 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -148,8 +148,8 @@ namespace StardewModdingAPI.Framework.Events /// Raised after objects are added or removed in a location. public readonly ManagedEvent ObjectListChanged; - /// Raised after a new item is added or removed from a chest in a location. - public readonly ManagedEvent ChestItemsChanged; + /// Raised after items are added or removed from a chest. + public readonly ManagedEvent ChestInventoryChanged; /// Raised after terrain features (like floors and trees) are added or removed in a location. public readonly ManagedEvent TerrainFeatureListChanged; @@ -224,7 +224,7 @@ namespace StardewModdingAPI.Framework.Events this.LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); this.NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); - this.ChestItemsChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ChestItemChanged)); + this.ChestInventoryChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ChestInventoryChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 8d6701ec..2ae69669 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -51,10 +51,11 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.ObjectListChanged.Remove(value); } - public event EventHandler ChestItemChanged + /// Raised after items are added or removed from a chest. + public event EventHandler ChestInventoryChanged { - add => this.EventManager.ChestItemsChanged.Add(value); - remove => this.EventManager.ChestItemsChanged.Remove(value); + add => this.EventManager.ChestInventoryChanged.Add(value); + remove => this.EventManager.ChestInventoryChanged.Remove(value); } /// Raised after terrain features (like floors and trees) are added or removed in a location. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 564fec1d..64da1cf1 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -703,8 +703,15 @@ namespace StardewModdingAPI.Framework events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); // chest items changed - if (locState.ChestItems.IsChanged) - events.ChestItemsChanged.Raise(new ChestItemChangedEventArgs(location, locState.ChestItems.Added, locState.ChestItems.Removed, locState.ChestItems.Key)); + if (events.ChestInventoryChanged.HasListeners()) + { + foreach (var pair in locState.ChestItems) + { + var diff = pair.Value; + if (diff.IsChanged) + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(location, pair.Key, diff.Added, diff.Removed)); + } + } // terrain features changed if (locState.TerrainFeatures.IsChanged) diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs index b14cacdb..d4d5df50 100644 --- a/src/SMAPI/Framework/SnapshotListDiff.cs +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -29,18 +29,10 @@ namespace StardewModdingAPI.Framework /// The added values. public IEnumerable Added => this.AddedImpl; - public Microsoft.Xna.Framework.Vector2 Key; /********* ** Public methods *********/ - - public void Update(ICollectionWatcher watcher, Microsoft.Xna.Framework.Vector2 key) - { - this.Key = key; - this.Update(watcher.IsChanged, watcher.Removed, watcher.Added); - } - /// Update the snapshot. /// Whether the value changed since the last update. /// The removed values. diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs index cac66356..8aa0eab5 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs @@ -1,21 +1,18 @@ -using System; using System.Collections.Generic; using Netcode; namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { - internal class NetListWatcher : BaseDisposableWatcher, ICollectionWatcher + /// A watcher which detects changes to a net list field. + /// The list value type. + internal class NetListWatcher : BaseDisposableWatcher, ICollectionWatcher where TValue : class, INetObject { - - /********* ** Fields *********/ /// The field being watched. - private readonly NetList> Field; - - public TKey Key { get; } + private readonly NetList> Field; /// The pairs added since the last reset. private readonly IList AddedImpl = new List(); @@ -23,26 +20,47 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// The pairs removed since the last reset. private readonly IList 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 NetListWatcher(NetList> field, TKey key) + public NetListWatcher(NetList> field) { this.Field = field; - this.Key = key; field.OnElementChanged += this.OnElementChanged; field.OnArrayReplaced += this.OnArrayReplaced; } - public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; - - public IEnumerable Added => this.AddedImpl; + /// Set the current value as the baseline. + public void Reset() + { + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } - public IEnumerable Removed => this.RemovedImpl; + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } - public void Dispose() + /// Stop watching the field and release all references. + public override void Dispose() { if (!this.IsDisposed) { @@ -53,47 +71,37 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers base.Dispose(); } - public void Reset() - { - this.AddedImpl.Clear(); - this.RemovedImpl.Clear(); - } - - public void Update() - { - this.AssertNotDisposed(); - } /********* ** Private methods *********/ - private void OnArrayReplaced(NetList> list, IList before, IList after) + /// A callback invoked when the value list is replaced. + /// The net field whose values changed. + /// The previous list of values. + /// The new list of values. + private void OnArrayReplaced(NetList> list, IList oldValues, IList newValues) { this.AddedImpl.Clear(); this.RemovedImpl.Clear(); - foreach(var obj in after) - this.AddedImpl.Add(obj); + foreach (TValue value in newValues) + this.AddedImpl.Add(value); - foreach(var obj in before) - this.RemovedImpl.Add(obj); + foreach (TValue value in oldValues) + this.RemovedImpl.Add(value); } - private void OnElementChanged(NetList> list, int index, TValue oldValue, TValue newValue) + /// A callback invoked when an entry is replaced. + /// The net field whose values changed. + /// The list index which changed. + /// The previous value. + /// The new value. + private void OnElementChanged(NetList> list, int index, TValue oldValue, TValue newValue) { - - /* checks for stack addition / subtraction changing stacks does not fire off an element changed event - if ((oldValue != null && newValue != null) && oldValue.CompareTo(newValue) < 0) - this.AddedImpl.Add(newValue); - //Stack Removed from - if ((oldValue != null && newValue != null) && oldValue.CompareTo(newValue) > 0) - this.RemovedImpl.Add(newValue); - */ - - if(newValue!=null) + if (newValue != null) this.AddedImpl.Add(newValue); - if(oldValue!=null) + if (oldValue != null) this.RemovedImpl.Add(oldValue); } } diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index ef4a8b64..170fd537 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -5,9 +5,9 @@ using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; +using StardewValley.Objects; using StardewValley.TerrainFeatures; -using Object = StardewValley.Object; -using Chest = StardewValley.Objects.Chest; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework.StateTracking { @@ -43,12 +43,14 @@ namespace StardewModdingAPI.Framework.StateTracking public ICollectionWatcher NpcsWatcher { get; } /// Tracks added or removed objects. - public IDictionaryWatcher ObjectsWatcher { get; } + public IDictionaryWatcher ObjectsWatcher { get; } /// Tracks added or removed terrain features. public IDictionaryWatcher TerrainFeaturesWatcher { get; } - public Dictionary> activeChestWatchers = new Dictionary>(); + /// Tracks items added or removed to chests. + public Dictionary> ChestWatchers = new Dictionary>(); + /********* ** Public methods @@ -76,13 +78,8 @@ namespace StardewModdingAPI.Framework.StateTracking this.ObjectsWatcher, this.TerrainFeaturesWatcher }); - } - /// Stop watching the player fields and release all references. - public void Dispose() - { - foreach (IWatcher watcher in this.Watchers) - watcher.Dispose(); + this.UpdateChestWatcherList(added: location.Objects.Pairs, removed: new KeyValuePair[0]); } /// Update the current value if needed. @@ -91,24 +88,7 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (IWatcher watcher in this.Watchers) watcher.Update(); - foreach (KeyValuePair obj in this.ObjectsWatcher.Added.Where(p => p.Value is Chest)) - { - if (!this.activeChestWatchers.ContainsKey(obj.Key)) - { - //create a new watcher for chests items - Chest temp = obj.Value as Chest; - NetListWatcher tempItemWatcher = new NetListWatcher(temp.items, obj.Key); - this.Watchers.Add(tempItemWatcher); - this.activeChestWatchers.Add(obj.Key, tempItemWatcher); - } - } - - foreach (KeyValuePair obj in this.ObjectsWatcher.Removed) - { - this.activeChestWatchers.TryGetValue(obj.Key, out NetListWatcher tempItemWatcher); - this.Watchers.Remove(tempItemWatcher); - this.activeChestWatchers.Remove(obj.Key); - } + this.UpdateChestWatcherList(added: this.ObjectsWatcher.Added, removed: this.ObjectsWatcher.Removed); } /// Set the current value as the baseline. @@ -117,5 +97,43 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (IWatcher watcher in this.Watchers) watcher.Reset(); } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Update the watcher list for added or removed chests. + /// The objects added to the location. + /// The objects removed from the location. + private void UpdateChestWatcherList(IEnumerable> added, IEnumerable> removed) + { + // remove unused watchers + foreach (KeyValuePair pair in removed) + { + if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ICollectionWatcher watcher)) + { + this.Watchers.Remove(watcher); + this.ChestWatchers.Remove(pair.Key); + } + } + + // add new watchers + foreach (KeyValuePair pair in added) + { + if (pair.Value is Chest chest && !this.ChestWatchers.ContainsKey(pair.Key)) + { + ICollectionWatcher watcher = new NetListWatcher(chest.items); + this.Watchers.Add(watcher); + this.ChestWatchers.Add(pair.Key, watcher); + } + } + } } } diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index 31cf29c3..4074336b 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Buildings; @@ -33,7 +34,8 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots /// Tracks added or removed terrain features. public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); - public SnapshotListDiff ChestItems { get; } = new SnapshotListDiff(); + /// Tracks changed chest inventories. + public IDictionary> ChestItems { get; } = new Dictionary>(); /********* @@ -50,6 +52,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots /// The watcher to snapshot. public void Update(LocationTracker watcher) { + // main lists this.Buildings.Update(watcher.BuildingsWatcher); this.Debris.Update(watcher.DebrisWatcher); this.LargeTerrainFeatures.Update(watcher.LargeTerrainFeaturesWatcher); @@ -57,8 +60,19 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.Objects.Update(watcher.ObjectsWatcher); this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher); - foreach (var obj in watcher.activeChestWatchers) - this.ChestItems.Update(obj.Value, obj.Key); + // chest inventories + foreach (Vector2 key in this.ChestItems.Keys.ToArray()) + { + if (!watcher.ChestWatchers.ContainsKey(key)) + this.ChestItems.Remove(key); + } + foreach (var pair in watcher.ChestWatchers) + { + if (!this.ChestItems.TryGetValue(pair.Key, out var diff)) + this.ChestItems[pair.Key] = diff = new SnapshotListDiff(); + + diff.Update(pair.Value); + } } } } -- cgit From 2894b4322304022b1924e4554c762b560b66b614 Mon Sep 17 00:00:00 2001 From: wartech0 Date: Tue, 31 Dec 2019 01:36:18 -0600 Subject: reworking chest event handler --- src/SMAPI/Framework/StateTracking/ChestTracker.cs | 79 ++++++++++++++++++++++ .../Snapshots/WorldLocationsSnapshot.cs | 2 +- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/StateTracking/ChestTracker.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs new file mode 100644 index 00000000..4440bf4b --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using ChangeType = StardewModdingAPI.Events.ChangeType; +using Chest = StardewValley.Objects.Chest; + +namespace StardewModdingAPI.Framework.StateTracking +{ + internal class ChestTracker + { + /********* + ** Fields + *********/ + /// The chest's inventory as of the last reset. + private IDictionary PreviousInventory; + + /// The chest's inventory change as of the last update. + private IDictionary CurrentInventory; + + /********* + ** Accessors + *********/ + /// The chest being tracked + public Chest Chest { get; } + + /********* + ** Public methods + *********/ + public ChestTracker(Chest chest) + { + this.Chest = chest; + this.PreviousInventory = this.GetInventory(); + } + + public void Update() + { + this.CurrentInventory = this.GetInventory(); + } + + + public void Reset() + { + this.PreviousInventory = this.CurrentInventory; + } + + public IEnumerable GetInventoryChanges() + { + IDictionary previous = this.PreviousInventory; + Console.WriteLine(previous.Count); + IDictionary current = this.GetInventory(); + Console.WriteLine(current.Count); + 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 }; + } + } + + /********* + ** Private methods + *********/ + + private IDictionary GetInventory() + { + return this.Chest.items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs index 73ed2d8f..ed8001d6 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots foreach (LocationTracker locationWatcher in watcher.Locations) { if (!this.LocationsDict.TryGetValue(locationWatcher.Location, out LocationSnapshot snapshot)) - this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher.Location); + this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher); snapshot.Update(locationWatcher); } -- cgit From 0411dcf3db277ed0d3c9f0201b7554a7d61ed1e8 Mon Sep 17 00:00:00 2001 From: wartech0 Date: Tue, 31 Dec 2019 04:20:36 -0600 Subject: Finished chest events --- src/SMAPI/Events/ChestInventoryChangedEventArgs.cs | 20 ++++++-------------- src/SMAPI/Events/ItemStackChange.cs | 7 ++++++- src/SMAPI/Framework/SGame.cs | 4 +--- src/SMAPI/Framework/StateTracking/ChestTracker.cs | 6 +++--- src/SMAPI/Framework/StateTracking/LocationTracker.cs | 15 +++++++++------ .../StateTracking/Snapshots/LocationSnapshot.cs | 20 ++++++++++---------- .../Snapshots/WorldLocationsSnapshot.cs | 2 +- 7 files changed, 36 insertions(+), 38 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs index 0b54e909..a0c63fdc 100644 --- a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs +++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs @@ -16,17 +16,10 @@ namespace StardewModdingAPI.Events public GameLocation Location { get; } /// The tile position of the chest. - public Vector2 Tile { get; } - - /// The objects added to the location. - public IEnumerable Added { get; } - - /// The objects removed from the location. - public IEnumerable Removed { get; } - - /// Whether this is the location containing the local player. - public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + public StardewValley.Objects.Chest Chest { get; } + /// The inventory changes added to the chest. + public ItemStackChange[] Changes { get; } /********* ** Public methods @@ -36,12 +29,11 @@ namespace StardewModdingAPI.Events /// The tile position of the chest. /// The objects added to the location. /// The objects removed from the location. - internal ChestInventoryChangedEventArgs(GameLocation location, Vector2 tile, IEnumerable added, IEnumerable removed) + internal ChestInventoryChangedEventArgs(GameLocation location, StardewValley.Objects.Chest chest, ItemStackChange[] changes) { this.Location = location; - this.Tile = tile; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); + this.Chest = chest; + this.Changes = changes; } } } diff --git a/src/SMAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs index f9ae6df6..dbb529d6 100644 --- a/src/SMAPI/Events/ItemStackChange.cs +++ b/src/SMAPI/Events/ItemStackChange.cs @@ -16,5 +16,10 @@ namespace StardewModdingAPI.Events /// How the inventory slot changed. public ChangeType ChangeType { get; set; } + + public override string ToString() + { + return this.StackChange + " " + this.Item.Name + " " + this.ChangeType.ToString(); + } } -} \ No newline at end of file +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 64da1cf1..c62bcf84 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -707,9 +707,7 @@ namespace StardewModdingAPI.Framework { foreach (var pair in locState.ChestItems) { - var diff = pair.Value; - if (diff.IsChanged) - events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(location, pair.Key, diff.Added, diff.Removed)); + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(location, pair.Key, pair.Value)); } } diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index 4440bf4b..74039753 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -44,15 +44,15 @@ namespace StardewModdingAPI.Framework.StateTracking public void Reset() { - this.PreviousInventory = this.CurrentInventory; + if(this.CurrentInventory!=null) + this.PreviousInventory = this.CurrentInventory; } public IEnumerable GetInventoryChanges() { IDictionary previous = this.PreviousInventory; - Console.WriteLine(previous.Count); IDictionary current = this.GetInventory(); - Console.WriteLine(current.Count); + foreach (Item item in previous.Keys.Union(current.Keys)) { if (!previous.TryGetValue(item, out int prevStack)) diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 170fd537..659efc57 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.StateTracking public IDictionaryWatcher TerrainFeaturesWatcher { get; } /// Tracks items added or removed to chests. - public Dictionary> ChestWatchers = new Dictionary>(); + public Dictionary ChestWatchers = new Dictionary(); /********* @@ -89,6 +89,9 @@ namespace StardewModdingAPI.Framework.StateTracking watcher.Update(); this.UpdateChestWatcherList(added: this.ObjectsWatcher.Added, removed: this.ObjectsWatcher.Removed); + + foreach (var watcher in this.ChestWatchers) + watcher.Value.Update(); } /// Set the current value as the baseline. @@ -96,6 +99,9 @@ namespace StardewModdingAPI.Framework.StateTracking { foreach (IWatcher watcher in this.Watchers) watcher.Reset(); + + foreach (var watcher in this.ChestWatchers) + watcher.Value.Reset(); } /// Stop watching the player fields and release all references. @@ -117,9 +123,8 @@ namespace StardewModdingAPI.Framework.StateTracking // remove unused watchers foreach (KeyValuePair pair in removed) { - if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ICollectionWatcher watcher)) + if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ChestTracker watcher)) { - this.Watchers.Remove(watcher); this.ChestWatchers.Remove(pair.Key); } } @@ -129,9 +134,7 @@ namespace StardewModdingAPI.Framework.StateTracking { if (pair.Value is Chest chest && !this.ChestWatchers.ContainsKey(pair.Key)) { - ICollectionWatcher watcher = new NetListWatcher(chest.items); - this.Watchers.Add(watcher); - this.ChestWatchers.Add(pair.Key, watcher); + this.ChestWatchers.Add(pair.Key, new ChestTracker(chest)); } } } diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index 4074336b..62a56c84 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; +using StardewModdingAPI.Events; using StardewValley; using StardewValley.Buildings; using StardewValley.TerrainFeatures; @@ -35,7 +36,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); /// Tracks changed chest inventories. - public IDictionary> ChestItems { get; } = new Dictionary>(); + public IDictionary ChestItems { get; } = new Dictionary(); /********* @@ -61,17 +62,16 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher); // chest inventories - foreach (Vector2 key in this.ChestItems.Keys.ToArray()) - { - if (!watcher.ChestWatchers.ContainsKey(key)) - this.ChestItems.Remove(key); - } foreach (var pair in watcher.ChestWatchers) { - if (!this.ChestItems.TryGetValue(pair.Key, out var diff)) - this.ChestItems[pair.Key] = diff = new SnapshotListDiff(); - - diff.Update(pair.Value); + IEnumerable temp = pair.Value.GetInventoryChanges(); + if (temp.Any()) + if (this.ChestItems.ContainsKey(pair.Value.Chest)) + this.ChestItems[pair.Value.Chest] = pair.Value.GetInventoryChanges().ToArray(); + else + this.ChestItems.Add(pair.Value.Chest, pair.Value.GetInventoryChanges().ToArray()); + else + this.ChestItems.Remove(pair.Value.Chest); } } } diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs index ed8001d6..73ed2d8f 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots foreach (LocationTracker locationWatcher in watcher.Locations) { if (!this.LocationsDict.TryGetValue(locationWatcher.Location, out LocationSnapshot snapshot)) - this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher); + this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher.Location); snapshot.Update(locationWatcher); } -- cgit From 6bf99f0f81582ab6d6212dc21e8c36686ceb5a35 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 Dec 2019 17:32:46 -0500 Subject: minor refactoring --- src/SMAPI/Events/ChestInventoryChangedEventArgs.cs | 20 +++++++++----------- src/SMAPI/Events/ItemStackChange.cs | 5 ----- src/SMAPI/Framework/SGame.cs | 4 +--- src/SMAPI/Framework/StateTracking/ChestTracker.cs | 21 +++++++++++++-------- .../Framework/StateTracking/LocationTracker.cs | 6 +----- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 2 +- .../StateTracking/Snapshots/LocationSnapshot.cs | 12 +++++------- 7 files changed, 30 insertions(+), 40 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs index a0c63fdc..7771cd7c 100644 --- a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs +++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; using StardewValley; +using StardewValley.Objects; namespace StardewModdingAPI.Events { @@ -12,24 +10,24 @@ namespace StardewModdingAPI.Events /********* ** Accessors *********/ + /// The chest whose inventory changed. + public Chest Chest { get; } + /// The location containing the chest. public GameLocation Location { get; } - /// The tile position of the chest. - public StardewValley.Objects.Chest Chest { get; } - - /// The inventory changes added to the chest. + /// The inventory changes in the chest. public ItemStackChange[] Changes { get; } + /********* ** Public methods *********/ /// Construct an instance. + /// The chest whose inventory changed. /// The location containing the chest. - /// The tile position of the chest. - /// The objects added to the location. - /// The objects removed from the location. - internal ChestInventoryChangedEventArgs(GameLocation location, StardewValley.Objects.Chest chest, ItemStackChange[] changes) + /// The inventory changes in the chest. + internal ChestInventoryChangedEventArgs(Chest chest, GameLocation location, ItemStackChange[] changes) { this.Location = location; this.Chest = chest; diff --git a/src/SMAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs index dbb529d6..cb5d2b88 100644 --- a/src/SMAPI/Events/ItemStackChange.cs +++ b/src/SMAPI/Events/ItemStackChange.cs @@ -16,10 +16,5 @@ namespace StardewModdingAPI.Events /// How the inventory slot changed. public ChangeType ChangeType { get; set; } - - public override string ToString() - { - return this.StackChange + " " + this.Item.Name + " " + this.ChangeType.ToString(); - } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c62bcf84..eccb6387 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -706,9 +706,7 @@ namespace StardewModdingAPI.Framework if (events.ChestInventoryChanged.HasListeners()) { foreach (var pair in locState.ChestItems) - { - events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(location, pair.Key, pair.Value)); - } + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, pair.Value)); } // terrain features changed diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index 74039753..f907b9a5 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -1,15 +1,13 @@ -using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI.Enums; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; +using StardewValley.Objects; using ChangeType = StardewModdingAPI.Events.ChangeType; -using Chest = StardewValley.Objects.Chest; namespace StardewModdingAPI.Framework.StateTracking { + /// Tracks changes to a chest's items. internal class ChestTracker { /********* @@ -21,33 +19,39 @@ namespace StardewModdingAPI.Framework.StateTracking /// The chest's inventory change as of the last update. private IDictionary CurrentInventory; + /********* ** Accessors *********/ - /// The chest being tracked + /// The chest being tracked. public Chest Chest { get; } + /********* ** Public methods *********/ + /// Construct an instance. + /// The chest being tracked. public ChestTracker(Chest chest) { this.Chest = chest; this.PreviousInventory = this.GetInventory(); } + /// Update the current values if needed. public void Update() { this.CurrentInventory = this.GetInventory(); } - + /// Reset all trackers so their current values are the baseline. public void Reset() { - if(this.CurrentInventory!=null) + if (this.CurrentInventory != null) this.PreviousInventory = this.CurrentInventory; } + /// Get the inventory changes since the last update. public IEnumerable GetInventoryChanges() { IDictionary previous = this.PreviousInventory; @@ -64,10 +68,11 @@ namespace StardewModdingAPI.Framework.StateTracking } } + /********* ** Private methods *********/ - + /// Get the player's current inventory. private IDictionary GetInventory() { return this.Chest.items diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 659efc57..7445add9 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.StateTracking public IDictionaryWatcher TerrainFeaturesWatcher { get; } /// Tracks items added or removed to chests. - public Dictionary ChestWatchers = new Dictionary(); + public IDictionary ChestWatchers { get; } = new Dictionary(); /********* @@ -124,18 +124,14 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (KeyValuePair pair in removed) { if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ChestTracker watcher)) - { this.ChestWatchers.Remove(pair.Key); - } } // add new watchers foreach (KeyValuePair pair in added) { if (pair.Value is Chest chest && !this.ChestWatchers.ContainsKey(pair.Key)) - { this.ChestWatchers.Add(pair.Key, new ChestTracker(chest)); - } } } } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 6302a889..cd7d75ec 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework.StateTracking return this.Player.currentLocation ?? this.LastValidLocation; } - /// Get the player inventory changes between two states. + /// Get the inventory changes since the last update. public IEnumerable GetInventoryChanges() { IDictionary previous = this.PreviousInventory; diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index 62a56c84..4e08a1ac 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Buildings; +using StardewValley.Objects; using StardewValley.TerrainFeatures; namespace StardewModdingAPI.Framework.StateTracking.Snapshots @@ -36,7 +37,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); /// Tracks changed chest inventories. - public IDictionary ChestItems { get; } = new Dictionary(); + public IDictionary ChestItems { get; } = new Dictionary(); /********* @@ -64,12 +65,9 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots // chest inventories foreach (var pair in watcher.ChestWatchers) { - IEnumerable temp = pair.Value.GetInventoryChanges(); - if (temp.Any()) - if (this.ChestItems.ContainsKey(pair.Value.Chest)) - this.ChestItems[pair.Value.Chest] = pair.Value.GetInventoryChanges().ToArray(); - else - this.ChestItems.Add(pair.Value.Chest, pair.Value.GetInventoryChanges().ToArray()); + ItemStackChange[] changes = pair.Value.GetInventoryChanges().ToArray(); + if (changes.Length > 0) + this.ChestItems[pair.Value.Chest] = changes; else this.ChestItems.Remove(pair.Value.Chest); } -- cgit From 844efa32d49baa2ca58332cd26bbbe3cc772ad22 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Jan 2020 00:13:59 -0500 Subject: optimize chest watchers using net events --- src/SMAPI/Framework/StateTracking/ChestTracker.cs | 89 +++++++++++++++------- .../StateTracking/FieldWatchers/WatcherFactory.cs | 8 ++ .../Framework/StateTracking/LocationTracker.cs | 6 ++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 5 +- 4 files changed, 79 insertions(+), 29 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index f907b9a5..66dc61eb 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Objects; using ChangeType = StardewModdingAPI.Events.ChangeType; @@ -8,16 +11,22 @@ using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking { /// Tracks changes to a chest's items. - internal class ChestTracker + internal class ChestTracker : IDisposable { /********* ** Fields *********/ - /// The chest's inventory as of the last reset. - private IDictionary PreviousInventory; + /// The item stack sizes as of the last update. + private readonly IDictionary StackSizes; - /// The chest's inventory change as of the last update. - private IDictionary CurrentInventory; + /// Items added since the last update. + private readonly HashSet Added = new HashSet(new ObjectReferenceComparer()); + + /// Items removed since the last update. + private readonly HashSet Removed = new HashSet(new ObjectReferenceComparer()); + + /// The underlying inventory watcher. + private readonly ICollectionWatcher InventoryWatcher; /********* @@ -35,50 +44,74 @@ namespace StardewModdingAPI.Framework.StateTracking public ChestTracker(Chest chest) { this.Chest = chest; - this.PreviousInventory = this.GetInventory(); + this.InventoryWatcher = WatcherFactory.ForNetList(chest.items); + + this.StackSizes = this.Chest.items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); } /// Update the current values if needed. public void Update() { - this.CurrentInventory = this.GetInventory(); + // update watcher + this.InventoryWatcher.Update(); + foreach (Item item in this.InventoryWatcher.Added.Where(p => p != null)) + this.Added.Add(item); + foreach (Item item in this.InventoryWatcher.Removed.Where(p => p != null)) + { + if (!this.Added.Remove(item)) // item didn't change if it was both added and removed, so remove it from both lists + this.Removed.Add(item); + } + + // stop tracking removed stacks + foreach (Item item in this.Removed) + this.StackSizes.Remove(item); } /// Reset all trackers so their current values are the baseline. public void Reset() { - if (this.CurrentInventory != null) - this.PreviousInventory = this.CurrentInventory; + // update stack sizes + foreach (Item item in this.StackSizes.Keys.ToArray().Concat(this.Added)) + this.StackSizes[item] = item.Stack; + + // update watcher + this.InventoryWatcher.Reset(); + this.Added.Clear(); + this.Removed.Clear(); } /// Get the inventory changes since the last update. public IEnumerable GetInventoryChanges() { - IDictionary previous = this.PreviousInventory; - IDictionary current = this.GetInventory(); + // removed + foreach (Item item in this.Removed) + yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed }; + + // added + foreach (Item item in this.Added) + yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; - foreach (Item item in previous.Keys.Union(current.Keys)) + // stack size changed + foreach (var entry in this.StackSizes) { - 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 }; + Item item = entry.Key; + int prevStack = entry.Value; + + if (item.Stack != prevStack) + yield return new ItemStackChange { Item = item, StackChange = item.Stack - prevStack, ChangeType = ChangeType.StackChange }; } } - - /********* - ** Private methods - *********/ - /// Get the player's current inventory. - private IDictionary GetInventory() + /// Release watchers and resources. + public void Dispose() { - return this.Chest.items - .Where(n => n != null) - .Distinct() - .ToDictionary(n => n, n => n.Stack); + this.StackSizes.Clear(); + this.Added.Clear(); + this.Removed.Clear(); + this.InventoryWatcher.Dispose(); } } } diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index 314ff7f5..bde43486 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -82,6 +82,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers return new NetCollectionWatcher(collection); } + /// Get a watcher for a net list. + /// The value type. + /// The net list. + public static ICollectionWatcher ForNetList(NetList> collection) where T : class, INetObject + { + return new NetListWatcher(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 7445add9..519fe8f4 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -109,6 +109,9 @@ namespace StardewModdingAPI.Framework.StateTracking { foreach (IWatcher watcher in this.Watchers) watcher.Dispose(); + + foreach (var watcher in this.ChestWatchers.Values) + watcher.Dispose(); } @@ -124,7 +127,10 @@ namespace StardewModdingAPI.Framework.StateTracking foreach (KeyValuePair pair in removed) { if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ChestTracker watcher)) + { + watcher.Dispose(); this.ChestWatchers.Remove(pair.Key); + } } // add new watchers diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index cd7d75ec..d25f3345 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -115,9 +115,12 @@ namespace StardewModdingAPI.Framework.StateTracking } } - /// Stop watching the player fields and release all references. + /// Release watchers and resources. public void Dispose() { + this.PreviousInventory.Clear(); + this.CurrentInventory?.Clear(); + foreach (IWatcher watcher in this.Watchers) watcher.Dispose(); } -- cgit From c5cfcc6c9ff36a7fc05916de95889c21dc6849ea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Jan 2020 00:22:29 -0500 Subject: fix location snapshot not updated for removed chests --- src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index 4e08a1ac..edfba736 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -63,13 +63,12 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher); // chest inventories - foreach (var pair in watcher.ChestWatchers) + this.ChestItems.Clear(); + foreach (ChestTracker tracker in watcher.ChestWatchers.Values) { - ItemStackChange[] changes = pair.Value.GetInventoryChanges().ToArray(); + ItemStackChange[] changes = tracker.GetInventoryChanges().ToArray(); if (changes.Length > 0) - this.ChestItems[pair.Value.Chest] = changes; - else - this.ChestItems.Remove(pair.Value.Chest); + this.ChestItems[tracker.Chest] = changes; } } } -- cgit From 6766fcd0fd5f6982d1ffc91a46e0d4a5703315dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Jan 2020 00:48:44 -0500 Subject: fix NetList watcher not handling array replacement and conflicting changes correctly --- src/SMAPI/Framework/StateTracking/ChestTracker.cs | 4 +- .../StateTracking/FieldWatchers/NetListWatcher.cs | 61 +++++++++++++++++----- 2 files changed, 50 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index 66dc61eb..3ce70c17 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -57,9 +57,9 @@ namespace StardewModdingAPI.Framework.StateTracking { // update watcher this.InventoryWatcher.Update(); - foreach (Item item in this.InventoryWatcher.Added.Where(p => p != null)) + foreach (Item item in this.InventoryWatcher.Added) this.Added.Add(item); - foreach (Item item in this.InventoryWatcher.Removed.Where(p => p != null)) + foreach (Item item in this.InventoryWatcher.Removed) { if (!this.Added.Remove(item)) // item didn't change if it was both added and removed, so remove it from both lists this.Removed.Add(item); diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs index 8aa0eab5..0b4d3030 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Netcode; +using StardewModdingAPI.Framework.StateTracking.Comparers; namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { @@ -15,10 +16,10 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers private readonly NetList> Field; /// The pairs added since the last reset. - private readonly IList AddedImpl = new List(); + private readonly ISet AddedImpl = new HashSet(new ObjectReferenceComparer()); /// The pairs removed since the last reset. - private readonly IList RemovedImpl = new List(); + private readonly ISet RemovedImpl = new HashSet(new ObjectReferenceComparer()); /********* @@ -81,14 +82,19 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// The new list of values. private void OnArrayReplaced(NetList> list, IList oldValues, IList newValues) { - this.AddedImpl.Clear(); - this.RemovedImpl.Clear(); - - foreach (TValue value in newValues) - this.AddedImpl.Add(value); + ISet oldSet = new HashSet(oldValues, new ObjectReferenceComparer()); + ISet changed = new HashSet(newValues, new ObjectReferenceComparer()); - foreach (TValue value in oldValues) - this.RemovedImpl.Add(value); + foreach (TValue value in oldSet) + { + if (!changed.Contains(value)) + this.Remove(value); + } + foreach (TValue value in changed) + { + if (!oldSet.Contains(value)) + this.Add(value); + } } /// A callback invoked when an entry is replaced. @@ -98,11 +104,40 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers /// The new value. private void OnElementChanged(NetList> list, int index, TValue oldValue, TValue newValue) { - if (newValue != null) - this.AddedImpl.Add(newValue); + this.Remove(oldValue); + this.Add(newValue); + } + + /// Track an added item. + /// The value that was added. + private void Add(TValue value) + { + if (value == null) + return; + + if (this.RemovedImpl.Contains(value)) + { + this.AddedImpl.Remove(value); + this.RemovedImpl.Remove(value); + } + else + this.AddedImpl.Add(value); + } - if (oldValue != null) - this.RemovedImpl.Add(oldValue); + /// Track a removed item. + /// The value that was removed. + private void Remove(TValue value) + { + if (value == null) + return; + + if (this.AddedImpl.Contains(value)) + { + this.AddedImpl.Remove(value); + this.RemovedImpl.Remove(value); + } + else + this.RemovedImpl.Add(value); } } } -- cgit From e64a1220e309e8fc83e20833b8849de1721ec469 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Jan 2020 18:52:24 -0500 Subject: unify item diff logic for players & chests --- src/SMAPI/Events/ChestInventoryChangedEventArgs.cs | 21 +++++-- src/SMAPI/Events/InventoryChangedEventArgs.cs | 34 ++++------- src/SMAPI/Events/ItemStackChange.cs | 20 ------- src/SMAPI/Framework/SGame.cs | 12 ++-- src/SMAPI/Framework/SnapshotItemListDiff.cs | 66 ++++++++++++++++++++++ src/SMAPI/Framework/StateTracking/ChestTracker.cs | 26 ++------- src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 27 +++++---- .../StateTracking/Snapshots/LocationSnapshot.cs | 7 +-- .../StateTracking/Snapshots/PlayerSnapshot.cs | 15 ++++- 9 files changed, 135 insertions(+), 93 deletions(-) delete mode 100644 src/SMAPI/Events/ItemStackChange.cs create mode 100644 src/SMAPI/Framework/SnapshotItemListDiff.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs index 7771cd7c..4b4c4210 100644 --- a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs +++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using StardewValley; using StardewValley.Objects; @@ -16,8 +17,14 @@ namespace StardewModdingAPI.Events /// The location containing the chest. public GameLocation Location { get; } - /// The inventory changes in the chest. - public ItemStackChange[] Changes { get; } + /// The added item stacks. + public IEnumerable Added { get; } + + /// The removed item stacks. + public IEnumerable Removed { get; } + + /// The item stacks whose size changed. + public IEnumerable QuantityChanged { get; } /********* @@ -26,12 +33,16 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The chest whose inventory changed. /// The location containing the chest. - /// The inventory changes in the chest. - internal ChestInventoryChangedEventArgs(Chest chest, GameLocation location, ItemStackChange[] changes) + /// The added item stacks. + /// The removed item stacks. + /// The item stacks whose size changed. + internal ChestInventoryChangedEventArgs(Chest chest, GameLocation location, Item[] added, Item[] removed, ItemStackSizeChange[] quantityChanged) { this.Location = location; this.Chest = chest; - this.Changes = changes; + this.Added = added; + this.Removed = removed; + this.QuantityChanged = quantityChanged; } } } diff --git a/src/SMAPI/Events/InventoryChangedEventArgs.cs b/src/SMAPI/Events/InventoryChangedEventArgs.cs index 874c2e48..40cd4128 100644 --- a/src/SMAPI/Events/InventoryChangedEventArgs.cs +++ b/src/SMAPI/Events/InventoryChangedEventArgs.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using StardewValley; namespace StardewModdingAPI.Events @@ -14,13 +13,13 @@ namespace StardewModdingAPI.Events /// The player whose inventory changed. public Farmer Player { get; } - /// The added items. + /// The added item stacks. public IEnumerable Added { get; } - /// The removed items. + /// The removed item stacks. public IEnumerable Removed { get; } - /// The items whose stack sizes changed, with the relative change. + /// The item stacks whose size changed. public IEnumerable QuantityChanged { get; } /// Whether the affected player is the local one. @@ -32,28 +31,15 @@ namespace StardewModdingAPI.Events *********/ /// Construct an instance. /// The player whose inventory changed. - /// The inventory changes. - internal InventoryChangedEventArgs(Farmer player, ItemStackChange[] changedItems) + /// The added item stacks. + /// The removed item stacks. + /// The item stacks whose size changed. + internal InventoryChangedEventArgs(Farmer player, Item[] added, Item[] removed, ItemStackSizeChange[] quantityChanged) { this.Player = player; - this.Added = changedItems - .Where(n => n.ChangeType == ChangeType.Added) - .Select(p => p.Item) - .ToArray(); - - this.Removed = changedItems - .Where(n => n.ChangeType == ChangeType.Removed) - .Select(p => p.Item) - .ToArray(); - - this.QuantityChanged = changedItems - .Where(n => n.ChangeType == ChangeType.StackChange) - .Select(change => new ItemStackSizeChange( - item: change.Item, - oldSize: change.Item.Stack - change.StackChange, - newSize: change.Item.Stack - )) - .ToArray(); + this.Added = added; + this.Removed = removed; + this.QuantityChanged = quantityChanged; } } } diff --git a/src/SMAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs deleted file mode 100644 index cb5d2b88..00000000 --- a/src/SMAPI/Events/ItemStackChange.cs +++ /dev/null @@ -1,20 +0,0 @@ -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Represents an inventory slot that changed. - public class ItemStackChange - { - /********* - ** Accessors - *********/ - /// The item in the slot. - public Item Item { get; set; } - - /// The amount by which the item's stack size changed. - public int StackChange { get; set; } - - /// How the inventory slot changed. - public ChangeType ChangeType { get; set; } - } -} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index eccb6387..e2b22ba7 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -706,7 +706,10 @@ namespace StardewModdingAPI.Framework if (events.ChestInventoryChanged.HasListeners()) { foreach (var pair in locState.ChestItems) - events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, pair.Value)); + { + SnapshotItemListDiff diff = pair.Value; + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged)); + } } // terrain features changed @@ -747,12 +750,13 @@ namespace StardewModdingAPI.Framework } // raise player inventory changed - ItemStackChange[] changedItems = playerState.InventoryChanges.ToArray(); - if (changedItems.Any()) + if (playerState.Inventory.IsChanged) { + var inventory = playerState.Inventory; + if (this.Monitor.IsVerbose) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, changedItems)); + events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged)); } } } diff --git a/src/SMAPI/Framework/SnapshotItemListDiff.cs b/src/SMAPI/Framework/SnapshotItemListDiff.cs new file mode 100644 index 00000000..e8ab1b1e --- /dev/null +++ b/src/SMAPI/Framework/SnapshotItemListDiff.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// A snapshot of a tracked item list. + internal class SnapshotItemListDiff + { + /********* + ** Accessors + *********/ + /// Whether the item list changed. + public bool IsChanged { get; } + + /// The removed values. + public Item[] Removed { get; } + + /// The added values. + public Item[] Added { get; } + + /// The items whose stack sizes changed. + public ItemStackSizeChange[] QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// Update the snapshot. + /// The added values. + /// The removed values. + /// The items whose stack sizes changed. + public SnapshotItemListDiff(Item[] added, Item[] removed, ItemStackSizeChange[] sizesChanged) + { + this.Removed = removed; + this.Added = added; + this.QuantityChanged = sizesChanged; + + this.IsChanged = removed.Length > 0 || added.Length > 0 || sizesChanged.Length > 0; + } + + /// Get a snapshot diff if anything changed in the given data. + /// The added item stacks. + /// The removed item stacks. + /// The items with their previous stack sizes. + /// The inventory changes, or null if nothing changed. + /// Returns whether anything changed. + public static bool TryGetChanges(ISet added, ISet removed, IDictionary stackSizes, out SnapshotItemListDiff changes) + { + KeyValuePair[] sizesChanged = stackSizes.Where(p => p.Key.Stack != p.Value).ToArray(); + if (sizesChanged.Any() || added.Any() || removed.Any()) + { + changes = new SnapshotItemListDiff( + added: added.ToArray(), + removed: removed.ToArray(), + sizesChanged: sizesChanged.Select(p => new ItemStackSizeChange(p.Key, p.Value, p.Key.Stack)).ToArray() + ); + return true; + } + + changes = null; + return false; + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index 3ce70c17..65f58ee7 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; using StardewValley.Objects; -using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking { @@ -83,26 +81,12 @@ namespace StardewModdingAPI.Framework.StateTracking this.Removed.Clear(); } - /// Get the inventory changes since the last update. - public IEnumerable GetInventoryChanges() + /// Get the inventory changes since the last update, if anything changed. + /// The inventory changes, or null if nothing changed. + /// Returns whether anything changed. + public bool TryGetInventoryChanges(out SnapshotItemListDiff changes) { - // removed - foreach (Item item in this.Removed) - yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed }; - - // added - foreach (Item item in this.Added) - yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; - - // stack size changed - foreach (var entry in this.StackSizes) - { - Item item = entry.Key; - int prevStack = entry.Value; - - if (item.Stack != prevStack) - yield return new ItemStackChange { Item = item, StackChange = item.Stack - prevStack, ChangeType = ChangeType.StackChange }; - } + return SnapshotItemListDiff.TryGetChanges(added: this.Added, removed: this.Removed, stackSizes: this.StackSizes, out changes); } /// Release watchers and resources. diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index d25f3345..cf49a7c1 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -2,10 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Enums; -using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; -using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking { @@ -99,20 +98,24 @@ namespace StardewModdingAPI.Framework.StateTracking return this.Player.currentLocation ?? this.LastValidLocation; } - /// Get the inventory changes since the last update. - public IEnumerable GetInventoryChanges() + /// Get the inventory changes since the last update, if anything changed. + /// The inventory changes, or null if nothing changed. + /// Returns whether anything changed. + public bool TryGetInventoryChanges(out SnapshotItemListDiff changes) { - IDictionary previous = this.PreviousInventory; IDictionary current = this.GetInventory(); - foreach (Item item in previous.Keys.Union(current.Keys)) + + ISet added = new HashSet(new ObjectReferenceComparer()); + ISet removed = new HashSet(new ObjectReferenceComparer()); + foreach (Item item in this.PreviousInventory.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 }; + if (!this.PreviousInventory.ContainsKey(item)) + added.Add(item); + else if (!current.ContainsKey(item)) + removed.Add(item); } + + return SnapshotItemListDiff.TryGetChanges(added: added, removed: removed, stackSizes: this.PreviousInventory, out changes); } /// Release watchers and resources. diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs index edfba736..6ae52fd0 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Linq; using Microsoft.Xna.Framework; -using StardewModdingAPI.Events; using StardewValley; using StardewValley.Buildings; using StardewValley.Objects; @@ -37,7 +35,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); /// Tracks changed chest inventories. - public IDictionary ChestItems { get; } = new Dictionary(); + public IDictionary ChestItems { get; } = new Dictionary(); /********* @@ -66,8 +64,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.ChestItems.Clear(); foreach (ChestTracker tracker in watcher.ChestWatchers.Values) { - ItemStackChange[] changes = tracker.GetInventoryChanges().ToArray(); - if (changes.Length > 0) + if (tracker.TryGetInventoryChanges(out SnapshotItemListDiff changes)) this.ChestItems[tracker.Chest] = changes; } } diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs index 7bcd9f82..f0fb9485 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs @@ -10,6 +10,13 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots /// A frozen snapshot of a tracked player. internal class PlayerSnapshot { + /********* + ** Fields + *********/ + /// An empty item list diff. + private readonly SnapshotItemListDiff EmptyItemListDiff = new SnapshotItemListDiff(new Item[0], new Item[0], new ItemStackSizeChange[0]); + + /********* ** Accessors *********/ @@ -27,7 +34,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots .ToDictionary(skill => skill, skill => new SnapshotDiff()); /// Get a list of inventory changes. - public IEnumerable InventoryChanges { get; private set; } + public SnapshotItemListDiff Inventory { get; private set; } /********* @@ -47,7 +54,11 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.Location.Update(watcher.LocationWatcher); foreach (var pair in this.Skills) pair.Value.Update(watcher.SkillWatchers[pair.Key]); - this.InventoryChanges = watcher.GetInventoryChanges().ToArray(); + + this.Inventory = watcher.TryGetInventoryChanges(out SnapshotItemListDiff itemChanges) + ? itemChanges + : this.EmptyItemListDiff; + } } } -- cgit From 93459a5e37b26cc8d742878dd993543c43f70694 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 4 Jan 2020 22:08:01 -0500 Subject: fix new asset invalidation code not correctly handling interceptors which both load and edit --- .../Framework/Content/AssetInterceptorChange.cs | 6 ++++-- src/SMAPI/Framework/SGame.cs | 20 ++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs index 498afe36..037d9f89 100644 --- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs +++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs @@ -64,7 +64,8 @@ namespace StardewModdingAPI.Framework.Content { try { - return editor.CanEdit(asset); + if (editor.CanEdit(asset)) + return true; } catch (Exception ex) { @@ -77,7 +78,8 @@ namespace StardewModdingAPI.Framework.Content { try { - return loader.CanLoad(asset); + if (loader.CanLoad(asset)) + return true; } catch (Exception ex) { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index e2b22ba7..d6c3b836 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -18,6 +18,7 @@ using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Toolkit.Serialization; @@ -427,17 +428,24 @@ namespace StardewModdingAPI.Framework return; } - /********* ** Reload assets when interceptors are added/removed *********/ if (this.ReloadAssetInterceptorsQueue.Any()) { - this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + // get unique interceptors + AssetInterceptorChange[] interceptors = this.ReloadAssetInterceptorsQueue + .GroupBy(p => p.Instance, new ObjectReferenceComparer()) + .Select(p => p.First()) + .ToArray(); + this.ReloadAssetInterceptorsQueue.Clear(); + + // log summary + this.Monitor.Log("Invalidating cached assets for new editors & loaders..."); this.Monitor.Log( - "changed: " + " changed: " + string.Join(", ", - this.ReloadAssetInterceptorsQueue + interceptors .GroupBy(p => p.Mod) .OrderBy(p => p.Key.DisplayName) .Select(modGroup => @@ -448,8 +456,8 @@ namespace StardewModdingAPI.Framework ) ); - this.ContentCore.InvalidateCache(asset => this.ReloadAssetInterceptorsQueue.Any(p => p.CanIntercept(asset))); - this.ReloadAssetInterceptorsQueue.Clear(); + // reload affected assets + this.ContentCore.InvalidateCache(asset => interceptors.Any(p => p.CanIntercept(asset))); } /********* -- cgit From d804526d52e6caa3fbaf22ad043898174b0b25ae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 4 Jan 2020 22:32:23 -0500 Subject: remove DumpMetadata option --- docs/release-notes.md | 1 + src/SMAPI/Framework/Models/ModFolderExport.cs | 21 --------------------- src/SMAPI/Framework/Models/SConfig.cs | 6 +----- src/SMAPI/Framework/SCore.cs | 14 -------------- src/SMAPI/SMAPI.config.json | 6 ------ 5 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 src/SMAPI/Framework/Models/ModFolderExport.cs (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 464803b2..32082356 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -38,6 +38,7 @@ * Added direct `Console` access to paranoid mode warnings. * Improved error messages for `TargetParameterCountException` when using the reflection API. * `helper.Read/WriteSaveData` can now be used while a save is being loaded (e.g. within a `Specialized.LoadStageChanged` event). + * Removed `DumpMetadata` option. It was only meant for specific debugging cases, but players would occasionally enable it incorrectly and then report crashes. * Fixed private textures loaded from content packs not having their `Name` field set. * For SMAPI developers: diff --git a/src/SMAPI/Framework/Models/ModFolderExport.cs b/src/SMAPI/Framework/Models/ModFolderExport.cs deleted file mode 100644 index 3b8d451a..00000000 --- a/src/SMAPI/Framework/Models/ModFolderExport.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Metadata exported to the mod folder. - internal class ModFolderExport - { - /// When the export was generated. - public string Exported { get; set; } - - /// The absolute path of the mod folder. - public string ModFolderPath { get; set; } - - /// The game version which last loaded the mods. - public string GameVersion { get; set; } - - /// The SMAPI version which last loaded the mods. - public string ApiVersion { get; set; } - - /// The detected mods. - public IModMetadata[] Mods { get; set; } - } -} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 53939f8c..b1612aa4 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -25,8 +25,7 @@ namespace StardewModdingAPI.Framework.Models [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", [nameof(VerboseLogging)] = false, - [nameof(LogNetworkTraffic)] = false, - [nameof(DumpMetadata)] = false + [nameof(LogNetworkTraffic)] = false }; /// The default values for , to log changes if different. @@ -64,9 +63,6 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log network traffic. Best combined with , which includes network metadata. public bool LogNetworkTraffic { get; set; } - /// Whether to generate a file in the mods folder with detailed metadata about the detected mods. - public bool DumpMetadata { get; set; } - /// The colors to use for text written to the SMAPI console. public ColorSchemeConfig ConsoleColors { get; set; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6e7c8fcb..dfd77e16 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -435,20 +435,6 @@ namespace StardewModdingAPI.Framework mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); - // write metadata file - if (this.Settings.DumpMetadata) - { - ModFolderExport export = new ModFolderExport - { - Exported = DateTime.UtcNow.ToString("O"), - ApiVersion = Constants.ApiVersion.ToString(), - GameVersion = Constants.GameVersion.ToString(), - ModFolderPath = this.ModsPath, - Mods = mods - }; - this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export); - } - // check for updates this.CheckForUpdatesAsync(mods); } diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index a7381b91..824bb783 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -59,12 +59,6 @@ The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to */ "LogNetworkTraffic": false, - /** - * Whether to generate a 'SMAPI-latest.metadata-dump.json' file in the logs folder with the full mod - * metadata for detected mods. This is only needed when troubleshooting some cases. - */ - "DumpMetadata": false, - /** * The colors to use for text written to the SMAPI console. * -- cgit