diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-12-26 00:31:36 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-12-26 00:31:36 -0500 |
commit | 15d4b6310e3dd15c62f3faedbf1290b2db26fb59 (patch) | |
tree | 47d49a9c69628f0df1e688361f46bc5b46b3c0fd /src/SMAPI | |
parent | 5cc5f089b9645a60385ff293b5a7202f260bfc0f (diff) | |
parent | f19cc3aac1a781bf2f2d20bc9577c2fe929b1e96 (diff) | |
download | SMAPI-15d4b6310e3dd15c62f3faedbf1290b2db26fb59.tar.gz SMAPI-15d4b6310e3dd15c62f3faedbf1290b2db26fb59.tar.bz2 SMAPI-15d4b6310e3dd15c62f3faedbf1290b2db26fb59.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
32 files changed, 873 insertions, 385 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 41b79272..786ae32b 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.2"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.3"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 54ce9b53..a5325b76 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -16,7 +16,7 @@ namespace StardewModdingAPI.Events public SButton Button { get; } /// <summary>The current cursor position.</summary> - public ICursorPosition Cursor { get; set; } + public ICursorPosition Cursor { get; } /// <summary>Whether the input is considered a 'click' by the game for enabling action.</summary> [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 @@ -28,6 +28,9 @@ namespace StardewModdingAPI.Events /// <summary>Whether the input should use tools on the affected tile.</summary> public bool IsUseToolButton { get; } + /// <summary>Whether a mod has indicated the key was already handled.</summary> + public bool IsSuppressed { get; private set; } + /********* ** Public methods @@ -55,6 +58,9 @@ namespace StardewModdingAPI.Events /// <param name="button">The button to suppress.</param> public void SuppressButton(SButton button) { + if (button == this.Button) + this.IsSuppressed = true; + // keyboard if (button.TryGetKeyboard(out Keys key)) Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs index b477376e..3466470d 100644 --- a/src/SMAPI/Events/GameEvents.cs +++ b/src/SMAPI/Events/GameEvents.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events /// <summary>Raised every 60th tick (≈once per second).</summary> public static event EventHandler OneSecondTick; + /// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary> + public static event EventHandler FirstUpdateTick; + /********* ** Internal methods @@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); } + + /// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + internal static void InvokeFirstUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList()); + } } } 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<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue(); + this.Cache = reflection.GetField<Dictionary<string, object>>(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<string>(path); } else diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index b07c6c7d..7a824a05 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework /// <param name="severity">How deprecated the code is.</param> public void Warn(string nounPhrase, string version, DeprecationLevel severity) { - this.Warn(this.ModRegistry.GetModFromStack(), nounPhrase, version, severity); + this.Warn(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version, severity); } /// <summary>Log a deprecation warning.</summary> @@ -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}"; @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework /// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns> public bool MarkWarned(string nounPhrase, string version) { - return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version); + return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version); } /// <summary>Mark a deprecation warning as already logged.</summary> diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index c21734a7..a36994fd 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework /// <summary>The mod instance (if it was loaded).</summary> IMod Mod { get; } + /// <summary>The mod-provided API (if any).</summary> + object Api { get; } + /********* ** Public methods @@ -43,5 +46,9 @@ namespace StardewModdingAPI.Framework /// <summary>Set the mod instance.</summary> /// <param name="mod">The mod instance to set.</param> IModMetadata SetMod(IMod mod); + + /// <summary>Set the mod-provided API instance.</summary> + /// <param name="api">The mod-provided API.</param> + IModMetadata SetApi(object api); } } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 3709e05d..bec6c183 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -108,6 +108,15 @@ namespace StardewModdingAPI.Framework } } + /// <summary>Get the lowest exception in an exception stack.</summary> + /// <param name="exception">The exception from which to search.</param> + public static Exception GetInnermostException(this Exception exception) + { + while (exception.InnerException != null) + exception = exception.InnerException; + return exception; + } + /**** ** Sprite batch ****/ @@ -125,7 +134,7 @@ namespace StardewModdingAPI.Framework #endif // get result - return reflection.GetPrivateField<bool>(Game1.spriteBatch, fieldName).GetValue(); + return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue(); } } } diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9e824694..ea0dbb38 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers { @@ -11,6 +13,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>The underlying mod registry.</summary> private readonly ModRegistry Registry; + /// <summary>Encapsulates monitoring and logging for the mod.</summary> + private readonly IMonitor Monitor; + + /// <summary>The mod IDs for APIs accessed by this instanced.</summary> + private readonly HashSet<string> AccessedModApis = new HashSet<string>(); + + /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> + private readonly InterfaceProxyBuilder ProxyBuilder; + /********* ** Public methods @@ -18,16 +29,20 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Construct an instance.</summary> /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="registry">The underlying mod registry.</param> - public ModRegistryHelper(string modID, ModRegistry registry) + /// <param name="proxyBuilder">Generates proxy classes to access mod APIs through an arbitrary interface.</param> + /// <param name="monitor">Encapsulates monitoring and logging for the mod.</param> + public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor) : base(modID) { this.Registry = registry; + this.ProxyBuilder = proxyBuilder; + this.Monitor = monitor; } /// <summary>Get metadata for all loaded mods.</summary> public IEnumerable<IManifest> GetAll() { - return this.Registry.GetAll(); + return this.Registry.GetAll().Select(p => p.Manifest); } /// <summary>Get metadata for a loaded mod.</summary> @@ -35,14 +50,56 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns> public IManifest Get(string uniqueID) { - return this.Registry.Get(uniqueID); + return this.Registry.Get(uniqueID)?.Manifest; } /// <summary>Get whether a mod has been loaded.</summary> /// <param name="uniqueID">The mod's unique ID.</param> public bool IsLoaded(string uniqueID) { - return this.Registry.IsLoaded(uniqueID); + return this.Registry.Get(uniqueID) != null; + } + + /// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary> + public object GetApi(string uniqueID) + { + IModMetadata mod = this.Registry.Get(uniqueID); + if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID)) + this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace); + return mod?.Api; + } + + /// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary> + /// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam> + /// <param name="uniqueID">The mod's unique ID.</param> + public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class + { + // validate + if (!this.Registry.AreAllModsInitialised) + { + this.Monitor.Log("Tried to access a mod-provided API before all mods were initialised.", LogLevel.Error); + return null; + } + if (!typeof(TInterface).IsInterface) + { + this.Monitor.Log("Tried to map a mod-provided API to a class; must be a public interface.", LogLevel.Error); + return null; + } + if (!typeof(TInterface).IsPublic) + { + this.Monitor.Log("Tried to map a mod-provided API to a non-public interface; must be a public interface.", LogLevel.Error); + return null; + } + + // get raw API + object api = this.GetApi(uniqueID); + if (api == null) + return null; + + // get API of type + if (api is TInterface castApi) + return castApi; + return this.ProxyBuilder.CreateProxy<TInterface>(api, this.ModID, uniqueID); } } } 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 /// <summary>The mod name for error messages.</summary> private readonly string ModName; + /// <summary>Manages deprecation warnings.</summary> + private readonly DeprecationManager DeprecationManager; + /********* ** Public methods @@ -25,15 +28,88 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The mod name for error messages.</param> /// <param name="reflector">The underlying reflection helper.</param> - public ReflectionHelper(string modID, string modName, Reflector reflector) + /// <param name="deprecationManager">Manages deprecation warnings.</param> + public ReflectionHelper(string modID, string modName, Reflector reflector, DeprecationManager deprecationManager) : base(modID) { this.ModName = modName; this.Reflector = reflector; + this.DeprecationManager = deprecationManager; + } + + /// <summary>Get an instance field.</summary> + /// <typeparam name="TValue">The field type.</typeparam> + /// <param name="obj">The object which has the field.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField<TValue>(obj, name, required) + ); + } + + /// <summary>Get a static field.</summary> + /// <typeparam name="TValue">The field type.</typeparam> + /// <param name="type">The type which has the field.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField<TValue>(type, name, required) + ); + } + + /// <summary>Get an instance property.</summary> + /// <typeparam name="TValue">The property type.</typeparam> + /// <param name="obj">The object which has the property.</param> + /// <param name="name">The property name.</param> + /// <param name="required">Whether to throw an exception if the property is not found.</param> + public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty<TValue>(obj, name, required) + ); + } + + /// <summary>Get a static property.</summary> + /// <typeparam name="TValue">The property type.</typeparam> + /// <param name="type">The type which has the property.</param> + /// <param name="name">The property name.</param> + /// <param name="required">Whether to throw an exception if the property is not found.</param> + public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty<TValue>(type, name, required) + ); + } + + /// <summary>Get an instance method.</summary> + /// <param name="obj">The object which has the method.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + public IReflectedMethod GetMethod(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(obj, name, required) + ); + } + + /// <summary>Get a static method.</summary> + /// <param name="type">The type which has the method.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + public IReflectedMethod GetMethod(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(type, name, required) + ); } + /**** - ** Fields + ** Obsolete ****/ /// <summary>Get a private instance field.</summary> /// <typeparam name="TValue">The field type.</typeparam> @@ -41,11 +117,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> /// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns> + [Obsolete] public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateField<TValue>(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required); } /// <summary>Get a private static field.</summary> @@ -53,26 +129,23 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="type">The type which has the field.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [Obsolete] public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateField<TValue>(type, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateField<TValue>)this.GetField<TValue>(type, name, required); } - /**** - ** Properties - ****/ /// <summary>Get a private instance property.</summary> /// <typeparam name="TValue">The property type.</typeparam> /// <param name="obj">The object which has the property.</param> /// <param name="name">The property name.</param> /// <param name="required">Whether to throw an exception if the private property is not found.</param> + [Obsolete] public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateProperty<TValue>(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateProperty<TValue>)this.GetProperty<TValue>(obj, name, required); } /// <summary>Get a private static property.</summary> @@ -80,17 +153,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="type">The type which has the property.</param> /// <param name="name">The property name.</param> /// <param name="required">Whether to throw an exception if the private property is not found.</param> + [Obsolete] public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateProperty<TValue>(type, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateProperty<TValue>)this.GetProperty<TValue>(type, name, required); } - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ /// <summary>Get the value of a private instance field.</summary> /// <typeparam name="TValue">The field type.</typeparam> /// <param name="obj">The object which has the field.</param> @@ -101,9 +170,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>. /// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(object,string,bool)" /> instead. /// </remarks> + [Obsolete] public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true) { - IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required); return field != null ? field.GetValue() : default(TValue); @@ -119,64 +190,36 @@ namespace StardewModdingAPI.Framework.ModHelpers /// This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>. /// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(Type,string,bool)" /> instead. /// </remarks> + [Obsolete] public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true) { - IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(type, name, required); return field != null ? field.GetValue() : default(TValue); } - /**** - ** Methods - ****/ /// <summary>Get a private instance method.</summary> /// <param name="obj">The object which has the method.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [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); } /// <summary>Get a private static method.</summary> /// <param name="type">The type which has the method.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [Obsolete] public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(type, name, required) - ); - } - - /**** - ** Methods by signature - ****/ - /// <summary>Get a private instance method.</summary> - /// <param name="obj">The object which has the method.</param> - /// <param name="name">The field name.</param> - /// <param name="argumentTypes">The argument types of the method signature to find.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required) - ); - } - - /// <summary>Get a private static method.</summary> - /// <param name="type">The type which has the method.</param> - /// <param name="name">The field name.</param> - /// <param name="argumentTypes">The argument types of the method signature to find.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - 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 /// <typeparam name="T">The field value type.</typeparam> /// <param name="field">The field being accessed.</param> /// <returns>Returns the same field instance for convenience.</returns> - private IPrivateField<T> AssertAccessAllowed<T>(IPrivateField<T> field) + private IReflectedField<T> AssertAccessAllowed<T>(IReflectedField<T> field) { this.AssertAccessAllowed(field?.FieldInfo); return field; @@ -197,7 +240,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <typeparam name="T">The property value type.</typeparam> /// <param name="property">The property being accessed.</param> /// <returns>Returns the same property instance for convenience.</returns> - private IPrivateProperty<T> AssertAccessAllowed<T>(IPrivateProperty<T> property) + private IReflectedProperty<T> AssertAccessAllowed<T>(IReflectedProperty<T> property) { this.AssertAccessAllowed(property?.PropertyInfo); return property; @@ -206,7 +249,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Assert that mods can use the reflection helper to access the given member.</summary> /// <param name="method">The method being accessed.</param> /// <returns>Returns the same method instance for convenience.</returns> - private IPrivateMethod AssertAccessAllowed(IPrivateMethod method) + private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) { this.AssertAccessAllowed(method?.MethodInfo); return method; diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 5055da75..30fe211b 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The mod instance (if it was loaded).</summary> public IMod Mod { get; private set; } + /// <summary>The mod-provided API (if any).</summary> + public object Api { get; private set; } + /********* ** Public methods @@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading this.Mod = mod; return this; } + + /// <summary>Set the mod-provided API instance.</summary> + /// <param name="api">The mod-provided API.</param> + public IModMetadata SetApi(object api) + { + this.Api = api; + return this; + } } } diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 9dde7a20..453d2868 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -15,26 +15,34 @@ namespace StardewModdingAPI.Framework /// <summary>The registered mod data.</summary> private readonly List<IModMetadata> Mods = new List<IModMetadata>(); - /// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary> - private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>(); + /// <summary>An assembly full name => mod lookup.</summary> + private readonly IDictionary<string, IModMetadata> ModNamesByAssembly = new Dictionary<string, IModMetadata>(); + + /// <summary>Whether all mods have been initialised and their <see cref="IMod.Entry"/> method called.</summary> + public bool AreAllModsInitialised { get; set; } /********* ** Public methods *********/ - /**** - ** Basic metadata - ****/ + /// <summary>Register a mod as a possible source of deprecation warnings.</summary> + /// <param name="metadata">The mod metadata.</param> + public void Add(IModMetadata metadata) + { + this.Mods.Add(metadata); + this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; + } + /// <summary>Get metadata for all loaded mods.</summary> - public IEnumerable<IManifest> GetAll() + public IEnumerable<IModMetadata> GetAll() { - return this.Mods.Select(p => p.Manifest); + return this.Mods.Select(p => p); } /// <summary>Get metadata for a loaded mod.</summary> /// <param name="uniqueID">The mod's unique ID.</param> /// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns> - public IManifest Get(string uniqueID) + public IModMetadata Get(string uniqueID) { // normalise search ID if (string.IsNullOrWhiteSpace(uniqueID)) @@ -42,37 +50,13 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); - } - - /// <summary>Get whether a mod has been loaded.</summary> - /// <param name="uniqueID">The mod's unique ID.</param> - public bool IsLoaded(string uniqueID) - { - return this.Get(uniqueID) != null; - } - - /**** - ** Mod data - ****/ - /// <summary>Register a mod as a possible source of deprecation warnings.</summary> - /// <param name="metadata">The mod metadata.</param> - public void Add(IModMetadata metadata) - { - this.Mods.Add(metadata); - this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName; - } - - /// <summary>Get all enabled mods.</summary> - public IEnumerable<IModMetadata> GetMods() - { - return (from mod in this.Mods select mod); + return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); } - /// <summary>Get the friendly mod name which defines a type.</summary> + /// <summary>Get the mod metadata from one of its assemblies.</summary> /// <param name="type">The type to check.</param> /// <returns>Returns the mod name, or <c>null</c> if the type isn't part of a known mod.</returns> - public string GetModFrom(Type type) + public IModMetadata GetFrom(Type type) { // null if (type == null) @@ -89,7 +73,7 @@ namespace StardewModdingAPI.Framework /// <summary>Get the friendly name for the closest assembly registered as a source of deprecation warnings.</summary> /// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns> - public string GetModFromStack() + public IModMetadata GetFromStack() { // get stack frames StackTrace stack = new StackTrace(); @@ -101,9 +85,9 @@ namespace StardewModdingAPI.Framework foreach (StackFrame frame in frames) { MethodBase method = frame.GetMethod(); - string name = this.GetModFrom(method.ReflectedType); - if (name != null) - return name; + IModMetadata mod = this.GetFrom(method.ReflectedType); + if (mod != null) + return mod; } // no known assembly found diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs new file mode 100644 index 00000000..5abebc18 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> + internal class InterfaceProxyBuilder + { + /********* + ** Properties + *********/ + /// <summary>The CLR module in which to create proxy classes.</summary> + private readonly ModuleBuilder ModuleBuilder; + + /// <summary>The generated proxy types.</summary> + private readonly IDictionary<string, Type> GeneratedTypes = new Dictionary<string, Type>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public InterfaceProxyBuilder() + { + AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); + } + + /// <summary>Create an API proxy.</summary> + /// <typeparam name="TInterface">The interface through which to access the API.</typeparam> + /// <param name="instance">The API instance to access.</param> + /// <param name="sourceModID">The unique ID of the mod consuming the API.</param> + /// <param name="targetModID">The unique ID of the mod providing the API.</param> + public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) + where TInterface : class + { + // validate + if (instance == null) + throw new InvalidOperationException("Can't proxy access to a null API."); + if (!typeof(TInterface).IsInterface) + throw new InvalidOperationException("The proxy type must be an interface, not a class."); + + // get proxy type + Type targetType = instance.GetType(); + string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; + if (!this.GeneratedTypes.TryGetValue(proxyTypeName, out Type type)) + { + type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType); + this.GeneratedTypes[proxyTypeName] = type; + } + + // create instance + ConstructorInfo constructor = type.GetConstructor(new[] { targetType }); + if (constructor == null) + throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen + return (TInterface)constructor.Invoke(new[] { instance }); + } + + + /********* + ** Private methods + *********/ + /// <summary>Define a class which proxies access to a target type through an interface.</summary> + /// <param name="proxyTypeName">The name of the proxy type to generate.</param> + /// <param name="interfaceType">The interface type through which to access the target.</param> + /// <param name="targetType">The target type to access.</param> + private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType) + { + // define proxy type + TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class); + proxyBuilder.AddInterfaceImplementation(interfaceType); + + // create field to store target instance + FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); + + // create constructor which accepts target instance + { + ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); + ILGenerator il = constructor.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); // this + // ReSharper disable once AssignNullToNotNullAttribute -- never null + il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldarg_1); // load argument + il.Emit(OpCodes.Stfld, field); // set field to loaded argument + il.Emit(OpCodes.Ret); + } + + // proxy methods + foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) + { + var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); + if (targetMethod == null) + throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); + + this.ProxyMethod(proxyBuilder, targetMethod, field); + } + + // create type + return proxyBuilder.CreateType(); + } + + /// <summary>Define a method which proxies access to a method on the target.</summary> + /// <param name="proxyBuilder">The proxy type being generated.</param> + /// <param name="target">The target method.</param> + /// <param name="instanceField">The proxy field containing the API instance.</param> + private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) + { + Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray(); + + // create method + MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); + methodBuilder.SetParameters(argTypes); + methodBuilder.SetReturnType(target.ReturnType); + + // create method body + { + ILGenerator il = methodBuilder.GetILGenerator(); + + // load target instance + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, instanceField); + + // invoke target method on instance + for (int i = 0; i < argTypes.Length; i++) + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Call, target); + + // return result + il.Emit(OpCodes.Ret); + } + } + } +} diff --git a/src/SMAPI/Framework/Reflection/PrivateField.cs b/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 { - /// <summary>A private field obtained through reflection.</summary> + /// <summary>A field obtained through reflection.</summary> /// <typeparam name="TValue">The field value type.</typeparam> - internal class PrivateField<TValue> : IPrivateField<TValue> + internal class ReflectedField<TValue> : IPrivateField<TValue>, IReflectedField<TValue> { /********* ** Properties @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Reflection /// <param name="isStatic">Whether the field is static.</param> /// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="field"/> is null.</exception> /// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception> - 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 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 { - /// <summary>A private method obtained through reflection.</summary> - internal class PrivateMethod : IPrivateMethod + /// <summary>A method obtained through reflection.</summary> + internal class ReflectedMethod : IPrivateMethod, IReflectedMethod { /********* ** Properties @@ -33,10 +33,10 @@ namespace StardewModdingAPI.Framework.Reflection /// <param name="parentType">The type that has the method.</param> /// <param name="obj">The object that has the instance method(if applicable).</param> /// <param name="method">The reflection metadata.</param> - /// <param name="isStatic">Whether the field is static.</param> + /// <param name="isStatic">Whether the method is static.</param> /// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="method"/> is null.</exception> /// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static method, or not null for a static method.</exception> - 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 index be346d71..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 { - /// <summary>A private property obtained through reflection.</summary> + /// <summary>A property obtained through reflection.</summary> /// <typeparam name="TValue">The property value type.</typeparam> - internal class PrivateProperty<TValue> : IPrivateProperty<TValue> + internal class ReflectedProperty<TValue> : IPrivateProperty<TValue>, IReflectedProperty<TValue> { /********* ** Properties @@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Reflection private readonly string DisplayName; /// <summary>The underlying property getter.</summary> - private readonly Func<TValue> GetterDelegate; + private readonly Func<TValue> GetMethod; /// <summary>The underlying property setter.</summary> - private readonly Action<TValue> SetterDelegate; + private readonly Action<TValue> SetMethod; /********* @@ -31,13 +31,13 @@ namespace StardewModdingAPI.Framework.Reflection ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="parentType">The type that has the field.</param> - /// <param name="obj">The object that has the instance field (if applicable).</param> + /// <param name="parentType">The type that has the property.</param> + /// <param name="obj">The object that has the instance property (if applicable).</param> /// <param name="property">The reflection metadata.</param> - /// <param name="isStatic">Whether the field is static.</param> + /// <param name="isStatic">Whether the property is static.</param> /// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="property"/> is null.</exception> - /// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception> - public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) + /// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static property, or not null for a static property.</exception> + public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { // validate input if (parentType == null) @@ -55,24 +55,29 @@ namespace StardewModdingAPI.Framework.Reflection this.DisplayName = $"{parentType.FullName}::{property.Name}"; this.PropertyInfo = property; - this.GetterDelegate = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod); - this.SetterDelegate = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod); + if (this.PropertyInfo.GetMethod != null) + this.GetMethod = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod); + if (this.PropertyInfo.SetMethod != null) + this.SetMethod = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod); } /// <summary>Get the property value.</summary> public TValue GetValue() { + if (this.GetMethod == null) + throw new InvalidOperationException($"The {this.DisplayName} property has no get method."); + try { - return this.GetterDelegate(); + return this.GetMethod(); } 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); } } @@ -80,17 +85,20 @@ namespace StardewModdingAPI.Framework.Reflection //// <param name="value">The value to set.</param> public void SetValue(TValue value) { + if (this.SetMethod == null) + throw new InvalidOperationException($"The {this.DisplayName} property has no set method."); + try { - this.SetterDelegate(value); + this.SetMethod(value); } 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 { - /// <summary>Provides helper methods for accessing private game code.</summary> + /// <summary>Provides helper methods for accessing inaccessible code.</summary> /// <remarks>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).</remarks> internal class Reflector { @@ -25,139 +25,139 @@ namespace StardewModdingAPI.Framework.Reflection /**** ** Fields ****/ - /// <summary>Get a private instance field.</summary> + /// <summary>Get a instance field.</summary> /// <typeparam name="TValue">The field type.</typeparam> /// <param name="obj">The object which has the field.</param> /// <param name="name">The field name.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> /// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns> - public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true) + public IReflectedField<TValue> GetField<TValue>(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<TValue> field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(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; } - /// <summary>Get a private static field.</summary> + /// <summary>Get a static field.</summary> /// <typeparam name="TValue">The field type.</typeparam> /// <param name="type">The type which has the field.</param> /// <param name="name">The field name.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true) + /// <param name="required">Whether to throw an exception if the field is not found.</param> + public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateField<TValue> field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(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 ****/ - /// <summary>Get a private instance property.</summary> + /// <summary>Get a instance property.</summary> /// <typeparam name="TValue">The property type.</typeparam> /// <param name="obj">The object which has the property.</param> /// <param name="name">The property name.</param> - /// <param name="required">Whether to throw an exception if the private property is not found.</param> - public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true) + /// <param name="required">Whether to throw an exception if the property is not found.</param> + public IReflectedProperty<TValue> GetProperty<TValue>(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<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(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; } - /// <summary>Get a private static property.</summary> + /// <summary>Get a static property.</summary> /// <typeparam name="TValue">The property type.</typeparam> /// <param name="type">The type which has the property.</param> /// <param name="name">The property name.</param> - /// <param name="required">Whether to throw an exception if the private property is not found.</param> - public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true) + /// <param name="required">Whether to throw an exception if the property is not found.</param> + public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(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 ****/ - /// <summary>Get a private instance method.</summary> + /// <summary>Get a instance method.</summary> /// <param name="obj">The object which has the method.</param> /// <param name="name">The field name.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + /// <param name="required">Whether to throw an exception if the field is not found.</param> + 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; } - /// <summary>Get a private static method.</summary> + /// <summary>Get a static method.</summary> /// <param name="type">The type which has the method.</param> /// <param name="name">The field name.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + /// <param name="required">Whether to throw an exception if the field is not found.</param> + 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 ****/ - /// <summary>Get a private instance method.</summary> + /// <summary>Get a instance method.</summary> /// <param name="obj">The object which has the method.</param> /// <param name="name">The field name.</param> /// <param name="argumentTypes">The argument types of the method signature to find.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + /// <param name="required">Whether to throw an exception if the field is not found.</param> + 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; } - /// <summary>Get a private static method.</summary> + /// <summary>Get a static method.</summary> /// <param name="type">The type which has the method.</param> /// <param name="name">The field name.</param> /// <param name="argumentTypes">The argument types of the method signature to find.</param> - /// <param name="required">Whether to throw an exception if the private field is not found.</param> - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + /// <param name="required">Whether to throw an exception if the field is not found.</param> + 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 /// <param name="obj">The object which has the field.</param> /// <param name="name">The field name.</param> /// <param name="bindingFlags">The reflection binding which flags which indicates what type of field to find.</param> - private IPrivateField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); FieldInfo field = this.GetCached<FieldInfo>($"field::{isStatic}::{type.FullName}::{name}", () => @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return field != null - ? new PrivateField<TValue>(type, obj, field, isStatic) + ? new ReflectedField<TValue>(type, obj, field, isStatic) : null; } @@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework.Reflection /// <param name="obj">The object which has the property.</param> /// <param name="name">The property name.</param> /// <param name="bindingFlags">The reflection binding which flags which indicates what type of property to find.</param> - private IPrivateProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () => @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return property != null - ? new PrivateProperty<TValue>(type, obj, property, isStatic) + ? new ReflectedProperty<TValue>(type, obj, property, isStatic) : null; } @@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework.Reflection /// <param name="obj">The object which has the method.</param> /// <param name="name">The method name.</param> /// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param> - 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 /// <param name="name">The method name.</param> /// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param> /// <param name="argumentTypes">The argument types of the method signature to find.</param> - 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..ebea6c84 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework private readonly ContentCache Cache; /// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary> - private readonly IPrivateMethod GetKeyLocale; + private readonly IReflectedMethod GetKeyLocale; /// <summary>The language codes used in asset keys.</summary> private readonly IDictionary<string, LanguageCode> 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 @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework return this.LoadImpl<T>(assetName, instance); // load mod content - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}."); + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); try { return this.WithWriteLock(() => @@ -252,6 +252,8 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) when (!(ex is SContentLoadException)) { + if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") + throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex); } } @@ -413,7 +415,7 @@ namespace StardewModdingAPI.Framework private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection) { // get the private code field directly to avoid changed-code logic - IPrivateField<LanguageCode> codeField = reflection.GetPrivateField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); + IReflectedField<LanguageCode> codeField = reflection.GetField<LanguageCode>(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 c886a4b7..0a614f17 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 /// <summary>Used to access private fields and methods.</summary> - private static List<float> _fpsList => SGame.Reflection.GetPrivateField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue(); - private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); + private static List<float> _fpsList => SGame.Reflection.GetField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue(); + private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static float _fps { - set => SGame.Reflection.GetPrivateField<float>(typeof(Game1), nameof(_fps)).SetValue(value); + set => SGame.Reflection.GetField<float>(typeof(Game1), nameof(_fps)).SetValue(value); } - private static Task _newDayTask => SGame.Reflection.GetPrivateField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue(); - public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetPrivateField<BlendState>(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<Task>(typeof(Game1), nameof(_newDayTask)).GetValue(); + private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(bgColor)).GetValue(); + public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop + public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(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<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager + reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager } /**** @@ -557,9 +557,12 @@ namespace StardewModdingAPI.Framework /********* ** Update events *********/ - GameEvents.InvokeUpdateTick(this.Monitor); if (this.FirstUpdate) + { this.FirstUpdate = false; + GameEvents.InvokeFirstUpdateTick(this.Monitor); + } + GameEvents.InvokeUpdateTick(this.Monitor); if (this.CurrentUpdateTick % 2 == 0) GameEvents.InvokeSecondUpdateTick(this.Monitor); if (this.CurrentUpdateTick % 4 == 0) @@ -689,6 +692,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); activeClickableMenu.exitThisMenu(); } + this.RaisePostRender(); Game1.spriteBatch.End(); } //base.Draw(gameTime); @@ -712,6 +716,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); Game1.activeClickableMenu.exitThisMenu(); } + this.RaisePostRender(); Game1.spriteBatch.End(); if ((double)Game1.options.zoomLevel != 1.0) { @@ -721,11 +726,12 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } } else if ((int)Game1.gameMode == 11) { @@ -733,6 +739,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + this.RaisePostRender(); Game1.spriteBatch.End(); } else if (Game1.currentMinigame != null) @@ -744,6 +751,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); Game1.spriteBatch.End(); } + this.RaisePostRender(needsNewBatch: true); if ((double)Game1.options.zoomLevel != 1.0) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); @@ -752,11 +760,12 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } } else if (Game1.showingEndOfNightStuff) { @@ -775,6 +784,7 @@ namespace StardewModdingAPI.Framework Game1.activeClickableMenu.exitThisMenu(); } } + this.RaisePostRender(); Game1.spriteBatch.End(); if ((double)Game1.options.zoomLevel != 1.0) { @@ -784,11 +794,12 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } } else if ((int)Game1.gameMode == 6) { @@ -806,6 +817,7 @@ namespace StardewModdingAPI.Framework int x = 64; int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); + this.RaisePostRender(); Game1.spriteBatch.End(); if ((double)Game1.options.zoomLevel != 1.0) { @@ -815,11 +827,12 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } } else { @@ -1265,6 +1278,8 @@ namespace StardewModdingAPI.Framework } else if (Game1.farmEvent != null) Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + + this.RaisePostRender(); Game1.spriteBatch.End(); if (Game1.overlayMenu != null) { @@ -1272,14 +1287,6 @@ namespace StardewModdingAPI.Framework Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } - - if (GraphicsEvents.HasPostRenderListeners()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); - Game1.spriteBatch.End(); - } - this.renderScreenBuffer(); } } @@ -1401,5 +1408,19 @@ namespace StardewModdingAPI.Framework hash ^= v.GetHashCode(); return hash; } + + /// <summary>Raise the <see cref="GraphicsEvents.OnPostRenderEvent"/> if there are any listeners.</summary> + /// <param name="needsNewBatch">Whether to create a new sprite batch.</param> + private void RaisePostRender(bool needsNewBatch = false) + { + if (GraphicsEvents.HasPostRenderListeners()) + { + if (needsNewBatch) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + if (needsNewBatch) + Game1.spriteBatch.End(); + } + } } } diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index 35ac7c0f..44ef32c9 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI +namespace StardewModdingAPI { /// <summary>The implementation for a Stardew Valley mod.</summary> public interface IMod @@ -22,5 +22,8 @@ /// <summary>The mod entry point, called after the mod is first loaded.</summary> /// <param name="helper">Provides simplified APIs for writing mods.</param> void Entry(IModHelper helper); + + /// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary> + object GetApi(); } -}
\ No newline at end of file +} diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index 5ef3fd65..a06e099e 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace StardewModdingAPI { @@ -16,5 +16,14 @@ namespace StardewModdingAPI /// <summary>Get whether a mod has been loaded.</summary> /// <param name="uniqueID">The mod's unique ID.</param> bool IsLoaded(string uniqueID); + + /// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary> + /// <param name="uniqueID">The mod's unique ID.</param> + object GetApi(string uniqueID); + + /// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary> + /// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam> + /// <param name="uniqueID">The mod's unique ID.</param> + TInterface GetApi<TInterface>(string uniqueID) where TInterface : class; } -}
\ No newline at end of file +} 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 { /// <summary>A private field obtained through reflection.</summary> /// <typeparam name="TValue">The field value type.</typeparam> + [Obsolete("Use " + nameof(IReflectedField<TValue>) + " instead")] public interface IPrivateField<TValue> { /********* @@ -23,4 +25,4 @@ namespace StardewModdingAPI //// <param name="value">The value to set.</param> 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 { /// <summary>A private method obtained through reflection.</summary> + [Obsolete("Use " + nameof(IReflectedMethod) + " instead")] public interface IPrivateMethod { /********* @@ -24,4 +26,4 @@ namespace StardewModdingAPI /// <param name="arguments">The method arguments to pass in.</param> 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 { /// <summary>A private property obtained through reflection.</summary> /// <typeparam name="TValue">The property value type.</typeparam> + [Obsolete("Use " + nameof(IPrivateProperty<TValue>) + " instead")] public interface IPrivateProperty<TValue> { /********* @@ -23,4 +25,4 @@ namespace StardewModdingAPI //// <param name="value">The value to set.</param> 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 +{ + /// <summary>A field obtained through reflection.</summary> + /// <typeparam name="TValue">The field value type.</typeparam> + public interface IReflectedField<TValue> + { + /********* + ** Accessors + *********/ + /// <summary>The reflection metadata.</summary> + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Get the field value.</summary> + TValue GetValue(); + + /// <summary>Set the field value.</summary> + //// <param name="value">The value to set.</param> + 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 +{ + /// <summary>A method obtained through reflection.</summary> + public interface IReflectedMethod + { + /********* + ** Accessors + *********/ + /// <summary>The reflection metadata.</summary> + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Invoke the method.</summary> + /// <typeparam name="TValue">The return type.</typeparam> + /// <param name="arguments">The method arguments to pass in.</param> + TValue Invoke<TValue>(params object[] arguments); + + /// <summary>Invoke the method.</summary> + /// <param name="arguments">The method arguments to pass in.</param> + 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 +{ + /// <summary>A property obtained through reflection.</summary> + /// <typeparam name="TValue">The property value type.</typeparam> + public interface IReflectedProperty<TValue> + { + /********* + ** Accessors + *********/ + /// <summary>The reflection metadata.</summary> + PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Get the property value.</summary> + TValue GetValue(); + + /// <summary>Set the property value.</summary> + //// <param name="value">The value to set.</param> + 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 { - /// <summary>Provides an API for accessing private game code.</summary> + /// <summary>Provides an API for accessing inaccessible code.</summary> public interface IReflectionHelper : IModLinked { /********* ** Public methods *********/ + /// <summary>Get an instance field.</summary> + /// <typeparam name="TValue">The field type.</typeparam> + /// <param name="obj">The object which has the field.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true); + + /// <summary>Get a static field.</summary> + /// <typeparam name="TValue">The field type.</typeparam> + /// <param name="type">The type which has the field.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true); + + /// <summary>Get an instance property.</summary> + /// <typeparam name="TValue">The property type.</typeparam> + /// <param name="obj">The object which has the property.</param> + /// <param name="name">The property name.</param> + /// <param name="required">Whether to throw an exception if the property is not found.</param> + IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true); + + /// <summary>Get a static property.</summary> + /// <typeparam name="TValue">The property type.</typeparam> + /// <param name="type">The type which has the property.</param> + /// <param name="name">The property name.</param> + /// <param name="required">Whether to throw an exception if the property is not found.</param> + IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true); + + /// <summary>Get an instance method.</summary> + /// <param name="obj">The object which has the method.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + IReflectedMethod GetMethod(object obj, string name, bool required = true); + + /// <summary>Get a static method.</summary> + /// <param name="type">The type which has the method.</param> + /// <param name="name">The field name.</param> + /// <param name="required">Whether to throw an exception if the field is not found.</param> + IReflectedMethod GetMethod(Type type, string name, bool required = true); + + /***** + ** Obsolete + *****/ /// <summary>Get a private instance field.</summary> /// <typeparam name="TValue">The field type.</typeparam> /// <param name="obj">The object which has the field.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true); /// <summary>Get a private static field.</summary> @@ -20,6 +64,7 @@ namespace StardewModdingAPI /// <param name="type">The type which has the field.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true); /// <summary>Get a private instance property.</summary> @@ -27,6 +72,7 @@ namespace StardewModdingAPI /// <param name="obj">The object which has the property.</param> /// <param name="name">The property name.</param> /// <param name="required">Whether to throw an exception if the private property is not found.</param> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true); /// <summary>Get a private static property.</summary> @@ -34,6 +80,7 @@ namespace StardewModdingAPI /// <param name="type">The type which has the property.</param> /// <param name="name">The property name.</param> /// <param name="required">Whether to throw an exception if the private property is not found.</param> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true); /// <summary>Get the value of a private instance field.</summary> @@ -42,6 +89,7 @@ namespace StardewModdingAPI /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> /// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] TValue GetPrivateValue<TValue>(object obj, string name, bool required = true); /// <summary>Get the value of a private static field.</summary> @@ -50,18 +98,21 @@ namespace StardewModdingAPI /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> /// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] TValue GetPrivateValue<TValue>(Type type, string name, bool required = true); /// <summary>Get a private instance method.</summary> /// <param name="obj">The object which has the method.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); /// <summary>Get a private static method.</summary> /// <param name="type">The type which has the method.</param> /// <param name="name">The field name.</param> /// <param name="required">Whether to throw an exception if the private field is not found.</param> + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); } } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 3346f1ac..f285764c 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -50,7 +50,6 @@ namespace StardewModdingAPI.Metadata new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible), - new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs index ee75ba54..3a753afc 100644 --- a/src/SMAPI/Mod.cs +++ b/src/SMAPI/Mod.cs @@ -25,6 +25,9 @@ namespace StardewModdingAPI /// <param name="helper">Provides simplified APIs for writing mods.</param> public abstract void Entry(IModHelper helper); + /// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary> + public virtual object GetApi() => null; + /// <summary>Release or reset unmanaged resources.</summary> public void Dispose() { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3ba35e43..7eda9c66 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -247,7 +247,7 @@ namespace StardewModdingAPI this.IsDisposed = true; // dispose mod data - foreach (IModMetadata mod in this.ModRegistry.GetMods()) + foreach (IModMetadata mod in this.ModRegistry.GetAll()) { try { @@ -374,7 +374,7 @@ namespace StardewModdingAPI } // update window titles - int modsLoaded = this.ModRegistry.GetMods().Count(); + int modsLoaded = this.ModRegistry.GetAll().Count(); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; @@ -390,7 +390,7 @@ namespace StardewModdingAPI LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetMods()) + foreach (IModMetadata mod in this.ModRegistry.GetAll()) (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); } @@ -500,12 +500,11 @@ namespace StardewModdingAPI { // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version try { - this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; if (response.Error != null) { @@ -515,7 +514,7 @@ namespace StardewModdingAPI else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); else - this.VerboseLog(" OK."); + this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } catch (Exception ex) { @@ -527,95 +526,102 @@ namespace StardewModdingAPI } // check mod versions - try + if (mods.Any()) { - // log issues - if (this.Settings.VerboseLogging) + try { - this.VerboseLog("Validating mod update keys..."); - foreach (IModMetadata mod in mods) + // prepare update keys + Dictionary<string, IModMetadata[]> modsByKey = + ( + from mod in mods + where mod.Manifest?.UpdateKeys != null + from key in mod.Manifest.UpdateKeys + select new { key, mod } + ) + .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.Select(p => p.mod).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + + // report update keys { - if (mod.Manifest == null) - this.VerboseLog($" {mod.DisplayName}: no manifest."); - else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) - this.VerboseLog($" {mod.DisplayName}: no update keys."); + IModMetadata[] modsWithoutKeys = ( + from mod in mods + where + mod.Manifest != null + && (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) + && (mod.Manifest?.UniqueID != "SMAPI.ConsoleCommands" && mod.Manifest?.UniqueID != "SMAPI.TrainerMod") + orderby mod.DisplayName + select mod + ).ToArray(); + + string message = $"Checking {modsByKey.Count} mod update keys."; + if (modsWithoutKeys.Any()) + message += $" {modsWithoutKeys.Length} mods have no update keys: {string.Join(", ", modsWithoutKeys.Select(p => p.DisplayName))}."; + this.Monitor.Log($" {message}", LogLevel.Trace); } - } - // prepare update keys - Dictionary<string, IModMetadata[]> modsByKey = - ( - from mod in mods - where mod.Manifest?.UpdateKeys != null - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); + // fetch results + var results = + ( + from entry in client.GetModInfo(modsByKey.Keys.ToArray()) + from mod in modsByKey[entry.Key] + orderby mod.DisplayName + select new { entry.Key, Mod = mod, Info = entry.Value } + ) + .ToArray(); + + // extract latest versions + IDictionary<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>(); + foreach (var result in results) + { + IModMetadata mod = result.Mod; + ModInfoModel info = result.Info; - // fetch results - this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>(); - foreach (var result in results) - { - IModMetadata mod = result.Mod; - ModInfoModel info = result.Info; + // handle error + if (info.Error != null) + { + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); + continue; + } - // handle error - if (info.Error != null) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); - continue; + // track update + ISemanticVersion localVersion = mod.DataRecord != null + ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) + : mod.Manifest.Version; + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null + ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) + : info.Version + ); + bool isUpdate = latestVersion.IsNewerThan(localVersion); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "okay")}."); + if (isUpdate) + { + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) + updatesByMod[mod] = info; + } } - // track update - ISemanticVersion localVersion = mod.DataRecord != null - ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) - : mod.Manifest.Version; - ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null - ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) - : info.Version - ); - bool isUpdate = latestVersion.IsNewerThan(localVersion); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); - if (isUpdate) + // output + if (updatesByMod.Any()) { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) - updatesByMod[mod] = info; + this.Monitor.Newline(); + this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) + this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); } } - - // output - if (updatesByMod.Any()) + catch (Exception ex) { - this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); + this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? ex.Message + : ex.ToString() + ); } } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); - this.Monitor.Log(ex is WebException && ex.InnerException == null - ? ex.Message - : ex.ToString() - ); - } }).Start(); } @@ -649,6 +655,7 @@ namespace StardewModdingAPI AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder(); foreach (IModMetadata metadata in mods) { // get basic info @@ -690,53 +697,30 @@ namespace StardewModdingAPI continue; } - // validate assembly - try - { - int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); - if (modEntries == 0) - { - TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); - continue; - } - if (modEntries > 1) - { - TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); - continue; - } - } - catch (Exception ex) - { - TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); - continue; - } - // initialise mod try { - // get implementation - TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); - Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); - if (mod == null) - { - TrackSkip(metadata, "its entry class couldn't be instantiated."); - continue; - } - - // inject data + // init mod helpers + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IModHelper modHelper; { - 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); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); - - mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); - mod.Monitor = monitor; + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); } + // get mod instance + if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + continue; + + // init mod + mod.ModManifest = manifest; + mod.Helper = modHelper; + mod.Monitor = monitor; + // track mod metadata.SetMod(mod); this.ModRegistry.Add(metadata); @@ -747,7 +731,7 @@ namespace StardewModdingAPI } } } - IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray(); // log skipped mods this.Monitor.Newline(); @@ -811,6 +795,19 @@ namespace StardewModdingAPI { this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); } + + // get mod API + try + { + object api = metadata.Mod.GetApi(); + if (api != null) + this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); + metadata.SetApi(api); + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + } } // invalidate cache entries when needed @@ -846,13 +843,48 @@ namespace StardewModdingAPI this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(editors, loaders); } + + // unlock mod integrations + this.ModRegistry.AreAllModsInitialised = true; + } + + /// <summary>Load a mod's entry class.</summary> + /// <param name="modAssembly">The mod assembly.</param> + /// <param name="onError">A callback invoked when loading fails.</param> + /// <param name="mod">The loaded instance.</param> + private bool TryLoadModEntry(Assembly modAssembly, Action<string> onError, out Mod mod) + { + mod = null; + + // find type + TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); + if (modEntries.Length == 0) + { + onError($"its DLL has no '{nameof(Mod)}' subclass."); + return false; + } + if (modEntries.Length > 1) + { + onError($"its DLL contains multiple '{nameof(Mod)}' subclasses."); + return false; + } + + // get implementation + mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString()); + if (mod == null) + { + onError("its entry class couldn't be instantiated."); + return false; + } + + return true; } /// <summary>Reload translations for all mods.</summary> private void ReloadTranslations() { JsonHelper jsonHelper = new JsonHelper(); - foreach (IModMetadata metadata in this.ModRegistry.GetMods()) + foreach (IModMetadata metadata in this.ModRegistry.GetAll()) { // read translation files IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>(); diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 6718806e..18a9f978 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -1891,6 +1891,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateKeys": [ "Nexus:1401" ] }, { + // TrainerMod + "ID": "SMAPI.TrainerMod", + "Compatibility": { + "~": { + "Status": "Obsolete", + "ReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." + } + } + }, + { // Tree Transplant "ID": "TreeTransplant", "UpdateKeys": [ "Nexus:1342" ] diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 380ed733..f76ac439 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,10 +79,6 @@ <Private>True</Private> </Reference> <Reference Include="System.Windows.Forms" Condition="$(OS) == 'Windows_NT'" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> @@ -110,7 +106,11 @@ <Compile Include="Framework\ContentManagerShim.cs" /> <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> + <Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" /> <Compile Include="Framework\Utilities\ContextHash.cs" /> + <Compile Include="IReflectedField.cs" /> + <Compile Include="IReflectedMethod.cs" /> + <Compile Include="IReflectedProperty.cs" /> <Compile Include="Metadata\CoreAssets.cs" /> <Compile Include="ContentSource.cs" /> <Compile Include="Events\ContentEvents.cs" /> @@ -169,7 +169,7 @@ <Compile Include="Framework\Models\ModStatus.cs" /> <Compile Include="Framework\Models\SConfig.cs" /> <Compile Include="Framework\ModLoading\ModMetadata.cs" /> - <Compile Include="Framework\Reflection\PrivateProperty.cs" /> + <Compile Include="Framework\Reflection\ReflectedProperty.cs" /> <Compile Include="Framework\RequestExitDelegate.cs" /> <Compile Include="Framework\SContentManager.cs" /> <Compile Include="Framework\Exceptions\SParseException.cs" /> @@ -198,8 +198,8 @@ <Compile Include="Framework\Models\ModDataRecord.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoader.cs" /> <Compile Include="Framework\Reflection\CacheEntry.cs" /> - <Compile Include="Framework\Reflection\PrivateField.cs" /> - <Compile Include="Framework\Reflection\PrivateMethod.cs" /> + <Compile Include="Framework\Reflection\ReflectedField.cs" /> + <Compile Include="Framework\Reflection\ReflectedMethod.cs" /> <Compile Include="Framework\Reflection\Reflector.cs" /> <Compile Include="IManifest.cs" /> <Compile Include="IMod.cs" /> |