diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 10c41d08..4508e641 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -57,14 +57,14 @@ namespace StardewModdingAPI.Framework.Content public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) { // init - this.Cache = reflection.GetPrivateField>(contentManager, "loadedAssets").GetValue(); + this.Cache = reflection.GetField>(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"); + IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); this.NormaliseAssetNameForPlatform = path => method.Invoke(path); } else diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index b07c6c7d..20bb0d2d 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework return; // build message - string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; + string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase} is deprecated since SMAPI {version})."; if (source == null) message += $"{Environment.NewLine}{Environment.StackTrace}"; diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 3709e05d..f81e05a9 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -125,7 +125,7 @@ namespace StardewModdingAPI.Framework #endif // get result - return reflection.GetPrivateField(Game1.spriteBatch, fieldName).GetValue(); + return reflection.GetField(Game1.spriteBatch, fieldName).GetValue(); } } } diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 8788b142..81453003 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod name for error messages. private readonly string ModName; + /// Manages deprecation warnings. + private readonly DeprecationManager DeprecationManager; + /********* ** Public methods @@ -25,15 +28,88 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The unique ID of the relevant mod. /// The mod name for error messages. /// The underlying reflection helper. - public ReflectionHelper(string modID, string modName, Reflector reflector) + /// Manages deprecation warnings. + public ReflectionHelper(string modID, string modName, Reflector reflector, DeprecationManager deprecationManager) : base(modID) { this.ModName = modName; this.Reflector = reflector; + this.DeprecationManager = deprecationManager; } + /// Get an instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField(obj, name, required) + ); + } + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField(type, name, required) + ); + } + + /// Get an instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty(obj, name, required) + ); + } + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty(type, name, required) + ); + } + + /// Get an instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(obj, name, required) + ); + } + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(type, name, required) + ); + } + + /**** - ** Fields + ** Obsolete ****/ /// Get a private instance field. /// The field type. @@ -41,11 +117,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The field name. /// Whether to throw an exception if the private field is not found. /// Returns the field wrapper, or null if the field doesn't exist and is false. + [Obsolete] public IPrivateField GetPrivateField(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateField(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateField)this.GetField(obj, name, required); } /// Get a private static field. @@ -53,26 +129,23 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The type which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete] public IPrivateField GetPrivateField(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateField(type, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateField)this.GetField(type, name, required); } - /**** - ** Properties - ****/ /// Get a private instance property. /// The property type. /// The object which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete] public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateProperty(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateProperty)this.GetProperty(obj, name, required); } /// Get a private static property. @@ -80,17 +153,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The type which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete] public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateProperty(type, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateProperty)this.GetProperty(type, name, required); } - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ /// Get the value of a private instance field. /// The field type. /// The object which has the field. @@ -101,9 +170,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// This is a shortcut for followed by . /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. /// + [Obsolete] public TValue GetPrivateValue(object obj, string name, bool required = true) { - IPrivateField field = this.GetPrivateField(obj, name, required); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + IPrivateField field = (IPrivateField)this.GetField(obj, name, required); return field != null ? field.GetValue() : default(TValue); @@ -119,64 +190,36 @@ namespace StardewModdingAPI.Framework.ModHelpers /// This is a shortcut for followed by . /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. /// + [Obsolete] public TValue GetPrivateValue(Type type, string name, bool required = true) { - IPrivateField field = this.GetPrivateField(type, name, required); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + IPrivateField field = (IPrivateField)this.GetField(type, name, required); return field != null ? field.GetValue() : default(TValue); } - /**** - ** Methods - ****/ /// Get a private instance method. /// The object which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete] public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateMethod)this.GetMethod(obj, name, required); } /// Get a private static method. /// The type which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete] public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(type, name, required) - ); - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required) - ); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(type, name, argumentTypes, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateMethod)this.GetMethod(type, name, required); } @@ -187,7 +230,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The field value type. /// The field being accessed. /// Returns the same field instance for convenience. - private IPrivateField AssertAccessAllowed(IPrivateField field) + private IReflectedField AssertAccessAllowed(IReflectedField field) { this.AssertAccessAllowed(field?.FieldInfo); return field; @@ -197,7 +240,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The property value type. /// The property being accessed. /// Returns the same property instance for convenience. - private IPrivateProperty AssertAccessAllowed(IPrivateProperty property) + private IReflectedProperty AssertAccessAllowed(IReflectedProperty property) { this.AssertAccessAllowed(property?.PropertyInfo); return property; @@ -206,7 +249,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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) + private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) { this.AssertAccessAllowed(method?.MethodInfo); return method; diff --git a/src/SMAPI/Framework/Reflection/PrivateField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs similarity index 84% rename from src/SMAPI/Framework/Reflection/PrivateField.cs rename to src/SMAPI/Framework/Reflection/ReflectedField.cs index 0bf45969..ad1557bb 100644 --- a/src/SMAPI/Framework/Reflection/PrivateField.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { - /// A private field obtained through reflection. + /// A field obtained through reflection. /// The field value type. - internal class PrivateField : IPrivateField + internal class ReflectedField : IPrivateField, IReflectedField { /********* ** Properties @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Reflection /// Whether the field is static. /// The or is null. /// The is null for a non-static field, or not null for a static field. - public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic) + public ReflectedField(Type parentType, object obj, FieldInfo field, bool isStatic) { // validate if (parentType == null) @@ -64,11 +64,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); + throw new InvalidCastException($"Can't convert the {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't get the value of the {this.DisplayName} field", ex); } } @@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); + throw new InvalidCastException($"Can't assign the {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't set the value of the {this.DisplayName} field", ex); } } } diff --git a/src/SMAPI/Framework/Reflection/PrivateMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs similarity index 83% rename from src/SMAPI/Framework/Reflection/PrivateMethod.cs rename to src/SMAPI/Framework/Reflection/ReflectedMethod.cs index ba2374f4..376de869 100644 --- a/src/SMAPI/Framework/Reflection/PrivateMethod.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs @@ -3,8 +3,8 @@ using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { - /// A private method obtained through reflection. - internal class PrivateMethod : IPrivateMethod + /// A method obtained through reflection. + internal class ReflectedMethod : IPrivateMethod, IReflectedMethod { /********* ** Properties @@ -33,10 +33,10 @@ namespace StardewModdingAPI.Framework.Reflection /// The type that has the method. /// The object that has the instance method(if applicable). /// The reflection metadata. - /// Whether the field is static. + /// Whether the method is static. /// The or is null. /// The is null for a non-static method, or not null for a static method. - public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic) + public ReflectedMethod(Type parentType, object obj, MethodInfo method, bool isStatic) { // validate if (parentType == null) @@ -67,7 +67,7 @@ namespace StardewModdingAPI.Framework.Reflection } catch (Exception ex) { - throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); } // cast return value @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); + throw new InvalidCastException($"Can't convert the return value of the {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); } } @@ -92,8 +92,8 @@ namespace StardewModdingAPI.Framework.Reflection } catch (Exception ex) { - throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); } } } -} \ No newline at end of file +} diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs similarity index 73% rename from src/SMAPI/Framework/Reflection/PrivateProperty.cs rename to src/SMAPI/Framework/Reflection/ReflectedProperty.cs index c81f99c7..d6c964c1 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs @@ -3,9 +3,9 @@ using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { - /// A private property obtained through reflection. + /// A property obtained through reflection. /// The property value type. - internal class PrivateProperty : IPrivateProperty + internal class ReflectedProperty : IPrivateProperty, IReflectedProperty { /********* ** Properties @@ -31,13 +31,13 @@ namespace StardewModdingAPI.Framework.Reflection ** Public methods *********/ /// Construct an instance. - /// The type that has the field. - /// The object that has the instance field (if applicable). + /// The type that has the property. + /// The object that has the instance property (if applicable). /// The reflection metadata. - /// Whether the field is static. + /// Whether the property is static. /// The or is null. - /// The is null for a non-static field, or not null for a static field. - public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) + /// The is null for a non-static property, or not null for a static property. + public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { // validate input if (parentType == null) @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Framework.Reflection public TValue GetValue() { if (this.GetMethod == null) - throw new InvalidOperationException($"The private {this.DisplayName} property has no get method."); + throw new InvalidOperationException($"The {this.DisplayName} property has no get method."); try { @@ -73,11 +73,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); + throw new InvalidCastException($"Can't convert the {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex); + throw new Exception($"Couldn't get the value of the {this.DisplayName} property", ex); } } @@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Reflection public void SetValue(TValue value) { if (this.SetMethod == null) - throw new InvalidOperationException($"The private {this.DisplayName} property has no set method."); + throw new InvalidOperationException($"The {this.DisplayName} property has no set method."); try { @@ -94,11 +94,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); + throw new InvalidCastException($"Can't assign the {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex); + throw new Exception($"Couldn't set the value of the {this.DisplayName} property", ex); } } } diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 23a48505..910e3a54 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -5,7 +5,7 @@ using System.Runtime.Caching; namespace StardewModdingAPI.Framework.Reflection { - /// Provides helper methods for accessing private game code. + /// Provides helper methods for accessing inaccessible code. /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). internal class Reflector { @@ -25,139 +25,139 @@ namespace StardewModdingAPI.Framework.Reflection /**** ** Fields ****/ - /// Get a private instance field. + /// Get a instance field. /// The field type. /// The object which has the field. /// The field name. - /// Whether to throw an exception if the private field is not found. + /// Whether to throw an exception if the field is not found. /// 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) + public IReflectedField GetField(object obj, string name, bool required = true) { // validate if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object."); // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedField 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."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field."); return field; } - /// Get a private static field. + /// Get a static field. /// The field type. /// The type which has the field. /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + IReflectedField 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."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field."); return field; } /**** ** Properties ****/ - /// Get a private instance property. + /// Get a instance property. /// The property type. /// The object which has the property. /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(object obj, string name, bool required = true) { // validate if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object."); // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedProperty 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."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property."); return property; } - /// Get a private static property. + /// Get a static property. /// The property type. /// The type which has the property. /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + IReflectedProperty 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."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property."); return property; } /**** ** Methods ****/ - /// Get a private instance method. + /// Get a instance method. /// The object which has the method. /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, bool required = true) { // validate if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object."); // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedMethod 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."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method."); return method; } - /// Get a private static method. + /// Get a static method. /// The type which has the method. /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, bool required = true) { // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + IReflectedMethod 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."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method."); return method; } /**** ** Methods by signature ****/ - /// Get a private instance method. + /// Get a instance method. /// The object which has the method. /// The field name. /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, Type[] argumentTypes, bool required = true) { // validate parent if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object."); // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes); + ReflectedMethod 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."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method with that signature."); return method; } - /// Get a private static method. + /// Get a static method. /// The type which has the method. /// The field name. /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, Type[] argumentTypes, bool required = true) { // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes); + ReflectedMethod 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."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method with that signature."); return method; } @@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The object which has the field. /// The field name. /// The reflection binding which flags which indicates what type of field to find. - private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return field != null - ? new PrivateField(type, obj, field, isStatic) + ? new ReflectedField(type, obj, field, isStatic) : null; } @@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The object which has the property. /// The property name. /// The reflection binding which flags which indicates what type of property to find. - private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return property != null - ? new PrivateProperty(type, obj, property, isStatic) + ? new ReflectedProperty(type, obj, property, isStatic) : null; } @@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The object which has the method. /// The method name. /// The reflection binding which flags which indicates what type of method to find. - private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => @@ -226,7 +226,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return method != null - ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + ? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) : null; } @@ -236,7 +236,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The method name. /// The reflection binding which flags which indicates what type of method to find. /// The argument types of the method signature to find. - private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) + private ReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => @@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework.Reflection return methodInfo; }); return method != null - ? new PrivateMethod(type, obj, method, isStatic) + ? new ReflectedMethod(type, obj, method, isStatic) : null; } diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 524b2d17..1803098d 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework private readonly ContentCache Cache; /// The private method which generates the locale portion of an asset name. - private readonly IPrivateMethod GetKeyLocale; + private readonly IReflectedMethod GetKeyLocale; /// The language codes used in asset keys. private readonly IDictionary KeyLocales; @@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework // 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"); + this.GetKeyLocale = reflection.GetMethod(this, "languageCode"); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); // get asset data @@ -413,7 +413,7 @@ namespace StardewModdingAPI.Framework private IDictionary GetKeyLocales(Reflector reflection) { // get the private code field directly to avoid changed-code logic - IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + IReflectedField codeField = reflection.GetField(typeof(LocalizedContentManager), "_currentLangCode"); // remember previous settings LanguageCode previousCode = codeField.GetValue(); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 3062b0f6..e9777e0b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -133,20 +133,20 @@ namespace StardewModdingAPI.Framework // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. - private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); - private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); + private static List _fpsList => SGame.Reflection.GetField>(typeof(Game1), nameof(_fpsList)).GetValue(); + private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static float _fps { - set => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); + set => SGame.Reflection.GetField(typeof(Game1), nameof(_fps)).SetValue(value); } - private static Task _newDayTask => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); - public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); - private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(); - private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); + private static Task _newDayTask => SGame.Reflection.GetField(typeof(Game1), nameof(_newDayTask)).GetValue(); + private Color bgColor => SGame.Reflection.GetField(this, nameof(bgColor)).GetValue(); + public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop + public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(lightingBlend)).GetValue(); + private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); + private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke(); + private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); + private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming @@ -182,7 +182,7 @@ namespace StardewModdingAPI.Framework this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); - reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager + reflection.GetField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager } /**** diff --git a/src/SMAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs index 3e681c12..512bfdab 100644 --- a/src/SMAPI/IPrivateField.cs +++ b/src/SMAPI/IPrivateField.cs @@ -1,9 +1,11 @@ -using System.Reflection; +using System; +using System.Reflection; namespace StardewModdingAPI { /// A private field obtained through reflection. /// The field value type. + [Obsolete("Use " + nameof(IReflectedField) + " instead")] public interface IPrivateField { /********* @@ -23,4 +25,4 @@ namespace StardewModdingAPI //// The value to set. void SetValue(TValue value); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs index 67fc8b3c..b2fdaaeb 100644 --- a/src/SMAPI/IPrivateMethod.cs +++ b/src/SMAPI/IPrivateMethod.cs @@ -1,8 +1,10 @@ -using System.Reflection; +using System; +using System.Reflection; namespace StardewModdingAPI { /// A private method obtained through reflection. + [Obsolete("Use " + nameof(IReflectedMethod) + " instead")] public interface IPrivateMethod { /********* @@ -24,4 +26,4 @@ namespace StardewModdingAPI /// The method arguments to pass in. void Invoke(params object[] arguments); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs index 8d67fa7a..a24495dd 100644 --- a/src/SMAPI/IPrivateProperty.cs +++ b/src/SMAPI/IPrivateProperty.cs @@ -1,9 +1,11 @@ -using System.Reflection; +using System; +using System.Reflection; namespace StardewModdingAPI { /// A private property obtained through reflection. /// The property value type. + [Obsolete("Use " + nameof(IPrivateProperty) + " instead")] public interface IPrivateProperty { /********* @@ -23,4 +25,4 @@ namespace StardewModdingAPI //// The value to set. void SetValue(TValue value); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IReflectedField.cs b/src/SMAPI/IReflectedField.cs new file mode 100644 index 00000000..43ddad42 --- /dev/null +++ b/src/SMAPI/IReflectedField.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A field obtained through reflection. + /// The field value type. + public interface IReflectedField + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the field value. + TValue GetValue(); + + /// Set the field value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/SMAPI/IReflectedMethod.cs b/src/SMAPI/IReflectedMethod.cs new file mode 100644 index 00000000..de83b98c --- /dev/null +++ b/src/SMAPI/IReflectedMethod.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A method obtained through reflection. + public interface IReflectedMethod + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + TValue Invoke(params object[] arguments); + + /// Invoke the method. + /// The method arguments to pass in. + void Invoke(params object[] arguments); + } +} \ No newline at end of file diff --git a/src/SMAPI/IReflectedProperty.cs b/src/SMAPI/IReflectedProperty.cs new file mode 100644 index 00000000..73ad9f30 --- /dev/null +++ b/src/SMAPI/IReflectedProperty.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A property obtained through reflection. + /// The property value type. + public interface IReflectedProperty + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the property value. + TValue GetValue(); + + /// Set the property value. + //// The value to set. + void SetValue(TValue value); + } +} diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs index fb2c7861..fcebae42 100644 --- a/src/SMAPI/IReflectionHelper.cs +++ b/src/SMAPI/IReflectionHelper.cs @@ -1,18 +1,62 @@ -using System; +using System; namespace StardewModdingAPI { - /// Provides an API for accessing private game code. + /// Provides an API for accessing inaccessible code. public interface IReflectionHelper : IModLinked { /********* ** Public methods *********/ + /// Get an instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedField GetField(object obj, string name, bool required = true); + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedField GetField(Type type, string name, bool required = true); + + /// Get an instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + IReflectedProperty GetProperty(object obj, string name, bool required = true); + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + IReflectedProperty GetProperty(Type type, string name, bool required = true); + + /// Get an instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedMethod GetMethod(object obj, string name, bool required = true); + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedMethod GetMethod(Type type, string name, bool required = true); + + /***** + ** Obsolete + *****/ /// Get a private instance field. /// The field type. /// The object which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] IPrivateField GetPrivateField(object obj, string name, bool required = true); /// Get a private static field. @@ -20,6 +64,7 @@ namespace StardewModdingAPI /// The type which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] IPrivateField GetPrivateField(Type type, string name, bool required = true); /// Get a private instance property. @@ -27,6 +72,7 @@ namespace StardewModdingAPI /// The object which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true); /// Get a private static property. @@ -34,6 +80,7 @@ namespace StardewModdingAPI /// The type which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true); /// Get the value of a private instance field. @@ -42,6 +89,7 @@ namespace StardewModdingAPI /// The field name. /// Whether to throw an exception if the private field is not found. /// This is a shortcut for followed by . + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] TValue GetPrivateValue(object obj, string name, bool required = true); /// Get the value of a private static field. @@ -50,18 +98,21 @@ namespace StardewModdingAPI /// The field name. /// Whether to throw an exception if the private field is not found. /// This is a shortcut for followed by . + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] TValue GetPrivateValue(Type type, string name, bool required = true); /// Get a private instance method. /// The object which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); /// Get a private static method. /// The type which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3ba35e43..7bfb0abd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -728,7 +728,7 @@ namespace StardewModdingAPI IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 380ed733..0db94843 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -111,6 +111,9 @@ + + + @@ -169,7 +172,7 @@ - + @@ -198,8 +201,8 @@ - - + +