From 2ff937397163f0ad5940b636bc7312ac747d9c39 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 10:59:57 -0400 Subject: fix compatibility check crashing for players with SDV 1.08 --- src/SMAPI/Framework/GameVersion.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 48159f61..1884afe9 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace StardewModdingAPI.Framework @@ -22,6 +22,7 @@ namespace StardewModdingAPI.Framework ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", ["1.07a"] = "1.0.8-prerelease1", + ["1.08"] = "1.0.8", ["1.11"] = "1.1.1" }; -- cgit From 51a2c3991f3c76197afb21a42a30f2a91a7f9908 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 16:47:32 -0400 Subject: simplify SelectiveStringEnumConverter implementation --- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 7 +++--- .../Serialisation/SelectiveStringEnumConverter.cs | 25 +++++----------------- 2 files changed, 9 insertions(+), 23 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 3193aa3c..77b93b66 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; -using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.Serialisation { @@ -20,7 +19,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) + new SelectiveStringEnumConverter(), + new SelectiveStringEnumConverter(), + new SelectiveStringEnumConverter() } }; diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs index 37108556..e825c880 100644 --- a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs @@ -1,37 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System; using Newtonsoft.Json.Converters; namespace StardewModdingAPI.Framework.Serialisation { - /// A variant of which only converts certain enums. - internal class SelectiveStringEnumConverter : StringEnumConverter + /// A variant of which only converts a specified enum. + /// The enum type. + internal class SelectiveStringEnumConverter : StringEnumConverter { - /********* - ** Properties - *********/ - /// The enum type names to convert. - private readonly HashSet Types; - - /********* ** Public methods *********/ - /// Construct an instance. - /// The enum types to convert. - public SelectiveStringEnumConverter(params Type[] types) - { - this.Types = new HashSet(types.Select(p => p.FullName)); - } - /// Get whether this instance can convert the specified object type. /// The object type. public override bool CanConvert(Type type) { return base.CanConvert(type) - && this.Types.Contains((Nullable.GetUnderlyingType(type) ?? type).FullName); + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); } } } -- cgit From a4fb2331fe57102aa8e8b30efb8095a1edb6b923 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 Oct 2017 16:58:42 -0400 Subject: simplify JSON converter name --- src/SMAPI/Framework/Serialisation/JsonHelper.cs | 6 +++--- .../Serialisation/SelectiveStringEnumConverter.cs | 22 ---------------------- .../Framework/Serialisation/StringEnumConverter.cs | 22 ++++++++++++++++++++++ src/SMAPI/StardewModdingAPI.csproj | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) delete mode 100644 src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs create mode 100644 src/SMAPI/Framework/Serialisation/StringEnumConverter.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 77b93b66..d923ec0c 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -19,9 +19,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(), - new SelectiveStringEnumConverter(), - new SelectiveStringEnumConverter() + new StringEnumConverter(), + new StringEnumConverter(), + new StringEnumConverter() } }; diff --git a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs deleted file mode 100644 index e825c880..00000000 --- a/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Newtonsoft.Json.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// A variant of which only converts a specified enum. - /// The enum type. - internal class SelectiveStringEnumConverter : StringEnumConverter - { - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type type) - { - return - base.CanConvert(type) - && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs new file mode 100644 index 00000000..7afe86cd --- /dev/null +++ b/src/SMAPI/Framework/Serialisation/StringEnumConverter.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// A variant of which only converts a specified enum. + /// The enum type. + internal class StringEnumConverter : StringEnumConverter + { + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type type) + { + return + base.CanConvert(type) + && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index b8d5990e..6f7c2b3f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -173,7 +173,7 @@ - + -- cgit From 36b4e550f1945ef710fca2c6deab7df94e708ef7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 Oct 2017 21:26:00 -0400 Subject: fix e.SuppressButton() in input events not suppressing keyboard buttons --- src/SMAPI/Events/EventArgsInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 66cb19f2..617dac35 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Events { // keyboard if (this.Button.TryGetKeyboard(out Keys key)) - Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Except(new[] { key }).ToArray()); + Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); // controller else if (this.Button.TryGetController(out Buttons controllerButton)) -- cgit From 53df85f3123f8d9cb00013bb32b61c220ccad697 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 20 Oct 2017 16:37:22 -0400 Subject: enable access to public members using reflection API --- src/SMAPI/Framework/Reflection/Reflector.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 5c2d90fa..23a48505 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && field == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); return field; @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateField GetPrivateField(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); if (required && field == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); return field; @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && property == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); return property; @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && property == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); return property; @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); return method; @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); return method; @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Framework.Reflection throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); return method; @@ -155,7 +155,7 @@ namespace StardewModdingAPI.Framework.Reflection public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); return method; -- cgit From 85a8959e97e90b30ac8291904838e18f102e97c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 Oct 2017 21:51:48 -0400 Subject: fix mods which implement IAssetLoader being marked as conflicting with themselves --- src/SMAPI/Framework/SContentManager.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index db202567..2f5d104f 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -510,16 +510,12 @@ namespace StardewModdingAPI.Framework { foreach (var entry in entries) { - IModMetadata metadata = entry.Key; + IModMetadata mod = entry.Key; IList interceptors = entry.Value; - // special case if mod is an interceptor - if (metadata.Mod is T modAsInterceptor) - yield return new KeyValuePair(metadata, modAsInterceptor); - // registered editors foreach (T interceptor in interceptors) - yield return new KeyValuePair(metadata, interceptor); + yield return new KeyValuePair(mod, interceptor); } } -- cgit From f74321addc79a5616cc0f43e4f5f4b8154fac827 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 13:13:14 -0400 Subject: fix SMAPI blocking reflection access to vanilla members on overridden types (#371) --- src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 99 ++++++++++++++-------- 1 file changed, 66 insertions(+), 33 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 8d435416..8788b142 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -42,8 +43,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Returns the field wrapper, or null if the field doesn't exist and is false. public IPrivateField GetPrivateField(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateField(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateField(obj, name, required) + ); } /// Get a private static field. @@ -53,8 +55,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateField GetPrivateField(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateField(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateField(type, name, required) + ); } /**** @@ -67,8 +70,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateProperty(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateProperty(obj, name, required) + ); } /// Get a private static property. @@ -78,8 +82,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateProperty(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateProperty(type, name, required) + ); } /**** @@ -98,7 +103,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); IPrivateField field = this.GetPrivateField(obj, name, required); return field != null ? field.GetValue() @@ -117,7 +121,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); IPrivateField field = this.GetPrivateField(type, name, required); return field != null ? field.GetValue() @@ -133,8 +136,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(obj, name, required) + ); } /// Get a private static method. @@ -143,8 +147,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(type, name, required) + ); } /**** @@ -157,8 +162,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { - this.AssertAccessAllowed(obj); - return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required) + ); } /// Get a private static method. @@ -168,33 +174,60 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { - this.AssertAccessAllowed(type); - return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + return this.AssertAccessAllowed( + this.Reflector.GetPrivateMethod(type, name, argumentTypes, required) + ); } /********* ** Private methods *********/ - /// Assert that mods can use the reflection helper to access the given type. - /// The type being accessed. - private void AssertAccessAllowed(Type type) + /// Assert that mods can use the reflection helper to access the given member. + /// The field value type. + /// The field being accessed. + /// Returns the same field instance for convenience. + private IPrivateField AssertAccessAllowed(IPrivateField field) { - // validate type namespace - if (type.Namespace != null) - { - string rootSmapiNamespace = typeof(Program).Namespace; - if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) - throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); - } + this.AssertAccessAllowed(field?.FieldInfo); + return field; } - /// Assert that mods can use the reflection helper to access the given type. - /// The object being accessed. - private void AssertAccessAllowed(object obj) + /// Assert that mods can use the reflection helper to access the given member. + /// The property value type. + /// The property being accessed. + /// Returns the same property instance for convenience. + private IPrivateProperty AssertAccessAllowed(IPrivateProperty property) { - if (obj != null) - this.AssertAccessAllowed(obj.GetType()); + this.AssertAccessAllowed(property?.PropertyInfo); + return property; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The method being accessed. + /// Returns the same method instance for convenience. + private IPrivateMethod AssertAccessAllowed(IPrivateMethod method) + { + this.AssertAccessAllowed(method?.MethodInfo); + return method; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The member being accessed. + private void AssertAccessAllowed(MemberInfo member) + { + if (member == null) + return; + + // get type which defines the member + Type declaringType = member.DeclaringType; + if (declaringType == null) + throw new InvalidOperationException($"Can't validate access to {member.MemberType} {member.Name} because it has no declaring type."); // should never happen + + // validate access + string rootNamespace = typeof(Program).Namespace; + if (declaringType.Namespace == rootNamespace || declaringType.Namespace?.StartsWith(rootNamespace + ".") == true) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning. (Detected access to {declaringType.FullName}.{member.Name}.)"); } } } -- cgit From 99c8dd79406f5099194d72e26085a49939705259 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 15:07:06 -0400 Subject: add InputButton.ToSButton() extension --- src/SMAPI/SButton.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'src/SMAPI') diff --git a/src/SMAPI/SButton.cs b/src/SMAPI/SButton.cs index 0ec799db..bd6635c7 100644 --- a/src/SMAPI/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -615,6 +615,18 @@ namespace StardewModdingAPI return (SButton)(SButtonExtensions.ControllerOffset + key); } + /// Get the equivalent for the given button. + /// The Stardew Valley button to convert. + internal static SButton ToSButton(this InputButton input) + { + // derived from InputButton constructors + if (input.mouseLeft) + return SButton.MouseLeft; + if (input.mouseRight) + return SButton.MouseRight; + return input.key.ToSButton(); + } + /// Get the equivalent for the given button. /// The button to convert. /// The keyboard equivalent. -- cgit From ed56cb714d7fb76f3c1b9d2f2e7b7627f8accc70 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Oct 2017 15:09:36 -0400 Subject: replace input events' e.IsClick with better-designed e.IsActionButton and e.IsUseToolButton --- src/SMAPI/Events/EventArgsInput.cs | 18 +++++++++++++----- src/SMAPI/Events/InputEvents.cs | 15 ++++++++------- src/SMAPI/Framework/SGame.cs | 13 ++++++------- 3 files changed, 27 insertions(+), 19 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 617dac35..ff904675 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Events @@ -20,7 +19,14 @@ namespace StardewModdingAPI.Events public ICursorPosition Cursor { get; set; } /// Whether the input is considered a 'click' by the game for enabling action. - public bool IsClick { get; } + [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 + public bool IsClick => this.IsActionButton; + + /// Whether the input should trigger actions on the affected tile. + public bool IsActionButton { get; } + + /// Whether the input should use tools on the affected tile. + public bool IsUseToolButton { get; } /********* @@ -29,12 +35,14 @@ namespace StardewModdingAPI.Events /// Construct an instance. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { this.Button = button; this.Cursor = cursor; - this.IsClick = isClick; + this.IsActionButton = isActionButton; + this.IsUseToolButton = isUseToolButton; } /// Prevent the game from handling the vurrent button press. This doesn't prevent other mods from receiving the event. diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index c31eb698..985aed99 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -1,6 +1,5 @@ using System; using StardewModdingAPI.Framework; -using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Events { @@ -24,20 +23,22 @@ namespace StardewModdingAPI.Events /// Encapsulates monitoring and logging. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); } /// Raise a event. /// Encapsulates monitoring and logging. /// The button on the controller, keyboard, or mouse. /// The cursor position. - /// Whether the input is considered a 'click' by the game for enabling action. - internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + /// Whether the input should trigger actions on the affected tile. + /// Whether the input should use tools on the affected tile. + internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6f8f7cef..ca19d726 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -12,7 +12,6 @@ using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -371,7 +370,8 @@ namespace StardewModdingAPI.Framework SButton[] previousPressedKeys = this.PreviousPressedButtons; SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); - bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); + bool isUseToolButton = Game1.options.useToolButton.Any(p => framePressedKeys.Contains(p.ToSButton())); + bool isActionButton = !isUseToolButton && Game1.options.actionButton.Any(p => framePressedKeys.Contains(p.ToSButton())); // get cursor position ICursorPosition cursor; @@ -388,7 +388,7 @@ namespace StardewModdingAPI.Framework // raise button pressed foreach (SButton button in framePressedKeys) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isActionButton, isUseToolButton); // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -408,10 +408,9 @@ namespace StardewModdingAPI.Framework // raise button released foreach (SButton button in frameReleasedKeys) { - bool wasClick = - (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click - || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); + bool wasUseToolButton = (from opt in Game1.options.useToolButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any(); + bool wasActionButton = !wasUseToolButton && (from opt in Game1.options.actionButton let optButton = opt.ToSButton() where optButton == button && framePressedKeys.Contains(optButton) select optButton).Any(); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasActionButton, wasUseToolButton); // legacy events if (button.TryGetKeyboard(out Keys key)) -- cgit From 8c97a63a82729efe56d73928e9afb436dbffea56 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 23 Oct 2017 03:24:53 -0400 Subject: improve content manager thread safety, create content cache wrapper (#373) --- src/SMAPI/Framework/Content/ContentCache.cs | 150 ++++++++++++++++++ src/SMAPI/Framework/SContentManager.cs | 234 ++++++++++++++++------------ src/SMAPI/Framework/SGame.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 284 insertions(+), 103 deletions(-) create mode 100644 src/SMAPI/Framework/Content/ContentCache.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs new file mode 100644 index 00000000..10c41d08 --- /dev/null +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.Content +{ + /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localisation, loading content, etc. It assumes all keys passed in are already normalised. + internal class ContentCache + { + /********* + ** Properties + *********/ + /// The underlying asset cache. + private readonly IDictionary Cache; + + /// The possible directory separator characters in an asset key. + private readonly char[] PossiblePathSeparators; + + /// The preferred directory separator chaeacter in an asset key. + private readonly string PreferredPathSeparator; + + /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. + private readonly Func NormaliseAssetNameForPlatform; + + + /********* + ** Accessors + *********/ + /// Get or set the value of a raw cache entry. + /// The cache key. + public object this[string key] + { + get => this.Cache[key]; + set => this.Cache[key] = value; + } + + /// The current cache keys. + public IEnumerable Keys => this.Cache.Keys; + + + /********* + ** Public methods + *********/ + /**** + ** Constructor + ****/ + /// Construct an instance. + /// The underlying content manager whose cache to manage. + /// Simplifies access to private game code. + /// The possible directory separator characters in an asset key. + /// The preferred directory separator chaeacter in an asset key. + public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) + { + // init + this.Cache = reflection.GetPrivateField>(contentManager, "loadedAssets").GetValue(); + this.PossiblePathSeparators = possiblePathSeparators; + this.PreferredPathSeparator = preferredPathSeparator; + + // get key normalisation logic + if (Constants.TargetPlatform == Platform.Windows) + { + IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); + this.NormaliseAssetNameForPlatform = path => method.Invoke(path); + } + else + this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + } + + /**** + ** Fetch + ****/ + /// Get whether the cache contains a given key. + /// The cache key. + public bool ContainsKey(string key) + { + return this.Cache.ContainsKey(key); + } + + + /**** + ** Normalise + ****/ + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + string normalised = string.Join(this.PreferredPathSeparator, parts); + if (path.StartsWith(this.PreferredPathSeparator)) + normalised = this.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Normalise a cache key so it's consistent with the underlying cache. + /// The asset key. + [Pure] + public string NormaliseKey(string key) + { + key = this.NormalisePathSeparators(key); + return key.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase) + ? key.Substring(0, key.Length - 4) + : this.NormaliseAssetNameForPlatform(key); + } + + /**** + ** Remove + ****/ + /// Remove an asset with the given key. + /// The cache key. + /// Whether to dispose the entry value, if applicable. + /// Returns the removed key (if any). + public bool Remove(string key, bool dispose) + { + // get entry + if (!this.Cache.TryGetValue(key, out object value)) + return false; + + // dispose & remove entry + if (dispose && value is IDisposable disposable) + disposable.Dispose(); + + return this.Cache.Remove(key); + } + + /// 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 removed keys (if any). + public IEnumerable Remove(Func predicate, bool dispose = false) + { + List removed = new List(); + foreach (string key in this.Cache.Keys.ToArray()) + { + Type type = this.Cache[key].GetType(); + if (predicate(key, type)) + { + this.Remove(key, dispose); + removed.Add(key); + } + } + return removed; + } + } +} diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 2f5d104f..0b6daaa6 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.Xna.Framework; +using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; @@ -15,7 +15,17 @@ using StardewValley; namespace StardewModdingAPI.Framework { - /// SMAPI's implementation of the game's content manager which lets it raise content events. + /// A thread-safe content manager which intercepts assets being loaded to let SMAPI mods inject or edit them. + /// + /// This is the centralised content manager which manages all game assets. The game and mods don't use this class + /// directly; instead they use one of several instances, which proxy requests to + /// this class. That ensures that when the game disposes one content manager, the others can continue unaffected. + /// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously. + /// + /// Note that assets in the cache have two identifiers: the asset name (like "bundles") and key (like "bundles.pt-BR"). + /// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset + /// keys, and the game and mods only know about asset names. The content manager handles resolving them. + /// internal class SContentManager : LocalizedContentManager { /********* @@ -27,11 +37,8 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// The underlying content manager's asset cache. - private readonly IDictionary Cache; - - /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. - private readonly Func NormaliseAssetNameForPlatform; + /// The underlying asset cache. + private readonly ContentCache Cache; /// The private method which generates the locale portion of an asset name. private readonly IPrivateMethod GetKeyLocale; @@ -46,10 +53,10 @@ namespace StardewModdingAPI.Framework private readonly ContextHash AssetsBeingLoaded = new ContextHash(); /// A lookup of the content managers which loaded each asset. - private readonly IDictionary> AssetLoaders = new Dictionary>(); + private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>(); - /// An object locked to prevent concurrent changes to the underlying assets. - private readonly object Lock = new object(); + /// A lock used to prevents concurrent changes to the cache while data is being read. + private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); /********* @@ -77,30 +84,15 @@ namespace StardewModdingAPI.Framework /// The current culture for which to localise content. /// The current language code for which to localise content. /// Encapsulates monitoring and logging. - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) + /// Simplifies access to private code. + public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { - // validate - if (monitor == null) - throw new ArgumentNullException(nameof(monitor)); - - // initialise - var reflection = new Reflector(); - this.Monitor = monitor; - - // get underlying fields for interception - this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); + // init + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); - // get asset key normalisation logic - if (Constants.TargetPlatform == Platform.Windows) - { - IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); - this.NormaliseAssetNameForPlatform = path => method.Invoke(path); - } - else - this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic - // get asset data this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); @@ -108,34 +100,26 @@ namespace StardewModdingAPI.Framework /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. + [Pure] public string NormalisePathSeparators(string path) { - string[] parts = path.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - string normalised = string.Join(SContentManager.PreferredPathSeparator, parts); - if (path.StartsWith(SContentManager.PreferredPathSeparator)) - normalised = SContentManager.PreferredPathSeparator + normalised; // keep root slash - return normalised; + return this.Cache.NormalisePathSeparators(path); } /// Normalise an asset name so it's consistent with the underlying cache. /// The asset key. + [Pure] public string NormaliseAssetName(string assetName) { - assetName = this.NormalisePathSeparators(assetName); - if (assetName.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase)) - return assetName.Substring(0, assetName.Length - 4); - return this.NormaliseAssetNameForPlatform(assetName); + return this.Cache.NormaliseKey(assetName); } /// Get whether the content manager has already loaded and cached the given asset. /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) { - lock (this.Lock) - { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); - } + assetName = this.Cache.NormaliseKey(assetName); + return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName)); } /// Load an asset that has been processed by the content pipeline. @@ -152,10 +136,9 @@ namespace StardewModdingAPI.Framework /// The content manager instance for which to load the asset. public T LoadFor(string assetName, ContentManager instance) { - lock (this.Lock) + assetName = this.NormaliseAssetName(assetName); + return this.WithWriteLock(() => { - assetName = this.NormaliseAssetName(assetName); - // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) { @@ -186,7 +169,7 @@ namespace StardewModdingAPI.Framework this.Cache[assetName] = data; this.TrackAssetLoader(assetName, instance); return data; - } + }); } /// Inject an asset into the cache. @@ -195,12 +178,12 @@ namespace StardewModdingAPI.Framework /// The asset value. public void Inject(string assetName, T value) { - lock (this.Lock) + this.WithWriteLock(() => { assetName = this.NormaliseAssetName(assetName); this.Cache[assetName] = value; this.TrackAssetLoader(assetName, this); - } + }); } /// Get the current content locale. @@ -212,19 +195,11 @@ namespace StardewModdingAPI.Framework /// Get the cached asset keys. public IEnumerable GetAssetKeys() { - lock (this.Lock) - { - IEnumerable GetAllAssetKeys() - { - foreach (string cacheKey in this.Cache.Keys) - { - this.ParseCacheKey(cacheKey, out string assetKey, out string _); - yield return assetKey; - } - } - - return GetAllAssetKeys().Distinct(); - } + return this.WithReadLock(() => + this.Cache.Keys + .Select(this.GetAssetName) + .Distinct() + ); } /// Purge assets from the cache that match one of the interceptors. @@ -239,11 +214,12 @@ namespace StardewModdingAPI.Framework // 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((assetName, assetType) => { - // get asset metadata IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); // check loaders @@ -263,48 +239,44 @@ namespace StardewModdingAPI.Framework /// Returns whether any cache entries were invalidated. public bool InvalidateCache(Func predicate, bool dispose = false) { - lock (this.Lock) + return this.WithWriteLock(() => { - // find matching asset keys - HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string cacheKey in this.Cache.Keys) + // invalidate matching keys + HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => { - this.ParseCacheKey(cacheKey, out string assetKey, out _); - Type type = this.Cache[cacheKey].GetType(); - if (predicate(assetKey, type)) + this.ParseCacheKey(key, out string assetName, out _); + if (removeAssetNames.Contains(assetName) || predicate(assetName, type)) { - purgeAssetKeys.Add(assetKey); - purgeCacheKeys.Add(cacheKey); + removeAssetNames.Add(assetName); + removeKeys.Add(key); + return true; } - } + return false; + }); - // purge assets - foreach (string key in purgeCacheKeys) - { - if (dispose && this.Cache[key] is IDisposable disposable) - disposable.Dispose(); - this.Cache.Remove(key); - this.AssetLoaders.Remove(key); - } + // update reference tracking + foreach (string key in removeKeys) + this.ContentManagersByAssetKey.Remove(key); // reload core game assets int reloaded = 0; - foreach (string key in purgeAssetKeys) + foreach (string key in removeAssetNames) { if (this.CoreAssets.ReloadForKey(this, key)) reloaded++; } // report result - if (purgeCacheKeys.Any()) + if (removeKeys.Any()) { - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + this.Monitor.Log($"Invalidated {removeAssetNames.Count} asset names: {string.Join(", ", removeKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); return true; } this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); return false; - } + }); } /// Dispose assets for the given content manager shim. @@ -313,15 +285,26 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - foreach (var entry in this.AssetLoaders) - entry.Value.Remove(shim); - this.InvalidateCache((key, type) => !this.AssetLoaders[key].Any(), dispose: true); + this.WithWriteLock(() => + { + foreach (var entry in this.ContentManagersByAssetKey) + entry.Value.Remove(shim); + this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey[key].Any(), dispose: true); + }); } /********* ** Private methods *********/ + /// Dispose held resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); + base.Dispose(disposing); + } + /// Get whether an asset has already been loaded. /// The normalised asset name. private bool IsNormalisedKeyLoaded(string normalisedAssetName) @@ -335,8 +318,8 @@ namespace StardewModdingAPI.Framework /// The content manager that loaded the asset. private void TrackAssetLoader(string key, ContentManager manager) { - if (!this.AssetLoaders.TryGetValue(key, out HashSet hash)) - hash = this.AssetLoaders[key] = new HashSet(); + if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash)) + hash = this.ContentManagersByAssetKey[key] = new HashSet(); hash.Add(manager); } @@ -367,11 +350,19 @@ namespace StardewModdingAPI.Framework return map; } + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + /// Parse a cache key into its component parts. /// The input cache key. - /// The original asset key. + /// The original asset name. /// The asset locale code (or null if not localised). - private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) { // handle localised key if (!string.IsNullOrWhiteSpace(cacheKey)) @@ -382,7 +373,7 @@ namespace StardewModdingAPI.Framework string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); if (this.KeyLocales.ContainsKey(suffix))