From 1398e591abc23a7af927cc7de1e8df512b6fc598 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Dec 2017 12:46:10 -0500 Subject: [PATCH 01/34] fix reflection API error with properties which don't have both get and set --- docs/release-notes.md | 4 ++++ .../Framework/Reflection/PrivateProperty.cs | 20 +++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4cf8efa2..26a43f66 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,8 @@ # Release notes +## 2.3 +* For modders: + * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. + ## 2.2 * For players: * Fixed error when a mod loads custom assets on Linux/Mac. diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs index be346d71..c81f99c7 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/PrivateProperty.cs @@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Reflection private readonly string DisplayName; /// The underlying property getter. - private readonly Func GetterDelegate; + private readonly Func GetMethod; /// The underlying property setter. - private readonly Action SetterDelegate; + private readonly Action SetMethod; /********* @@ -55,16 +55,21 @@ namespace StardewModdingAPI.Framework.Reflection this.DisplayName = $"{parentType.FullName}::{property.Name}"; this.PropertyInfo = property; - this.GetterDelegate = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); - this.SetterDelegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); + if (this.PropertyInfo.GetMethod != null) + this.GetMethod = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); + if (this.PropertyInfo.SetMethod != null) + this.SetMethod = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); } /// Get the property value. public TValue GetValue() { + if (this.GetMethod == null) + throw new InvalidOperationException($"The private {this.DisplayName} property has no get method."); + try { - return this.GetterDelegate(); + return this.GetMethod(); } catch (InvalidCastException) { @@ -80,9 +85,12 @@ namespace StardewModdingAPI.Framework.Reflection //// The value to set. public void SetValue(TValue value) { + if (this.SetMethod == null) + throw new InvalidOperationException($"The private {this.DisplayName} property has no set method."); + try { - this.SetterDelegate(value); + this.SetMethod(value); } catch (InvalidCastException) { From dd7b5ac462f5be2d6bee9d61f243e5c32140f175 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 13:37:59 -0500 Subject: [PATCH 02/34] fix mods being able to change cursor position reported to other mods --- docs/release-notes.md | 1 + src/SMAPI/Events/EventArgsInput.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 26a43f66..16ed9af5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.3 * For modders: * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. + * Fixed issue where a mod could change the cursor position reported to other mods. ## 2.2 * For players: diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index 54ce9b53..ee15fd27 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -16,7 +16,7 @@ namespace StardewModdingAPI.Events public SButton Button { get; } /// The current cursor position. - public ICursorPosition Cursor { get; set; } + public ICursorPosition Cursor { get; } /// Whether the input is considered a 'click' by the game for enabling action. [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 From 2c5532f4ab0d0bf4ce5a4bc376cf8bb5fb803f11 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 13:43:05 -0500 Subject: [PATCH 03/34] add e.IsSuppressed to input event args --- docs/release-notes.md | 1 + src/SMAPI/Events/EventArgsInput.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 16ed9af5..02e75f3d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ # Release notes ## 2.3 * For modders: + * Added `IsSuppressed` to input events so mods can optionally avoid handling a key another mod already handled. * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. * Fixed issue where a mod could change the cursor position reported to other mods. diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index ee15fd27..a5325b76 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -28,6 +28,9 @@ namespace StardewModdingAPI.Events /// Whether the input should use tools on the affected tile. public bool IsUseToolButton { get; } + /// Whether a mod has indicated the key was already handled. + public bool IsSuppressed { get; private set; } + /********* ** Public methods @@ -55,6 +58,9 @@ namespace StardewModdingAPI.Events /// The button to suppress. 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()); From 80c4d93559989777fbe5a23b923155b93df7a715 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 15:28:27 -0500 Subject: [PATCH 04/34] fix GraphicsEvents.OnPostRenderEvent not being raised in some cases --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 74 ++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 02e75f3d..fa04c055 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.3 * For modders: * Added `IsSuppressed` to input events so mods can optionally avoid handling a key another mod already handled. + * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. * Fixed issue where a mod could change the cursor position reported to other mods. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index c886a4b7..3062b0f6 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -689,6 +689,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 +713,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 +723,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 +736,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 +748,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 +757,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 +781,7 @@ namespace StardewModdingAPI.Framework Game1.activeClickableMenu.exitThisMenu(); } } + this.RaisePostRender(); Game1.spriteBatch.End(); if ((double)Game1.options.zoomLevel != 1.0) { @@ -784,11 +791,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 +814,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 +824,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 +1275,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 +1284,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 +1405,19 @@ namespace StardewModdingAPI.Framework hash ^= v.GetHashCode(); return hash; } + + /// Raise the if there are any listeners. + /// Whether to create a new sprite batch. + 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(); + } + } } } From 8776d1afa6dce054f3bc7cb421c86f3e2fe06ab3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 18:05:18 -0500 Subject: [PATCH 05/34] adjust reflection API to correctly reflect what it does (#410) --- src/SMAPI/Framework/Content/ContentCache.cs | 4 +- src/SMAPI/Framework/DeprecationManager.cs | 2 +- src/SMAPI/Framework/InternalExtensions.cs | 2 +- .../Framework/ModHelpers/ReflectionHelper.cs | 167 +++++++++++------- .../{PrivateField.cs => ReflectedField.cs} | 16 +- .../{PrivateMethod.cs => ReflectedMethod.cs} | 16 +- ...rivateProperty.cs => ReflectedProperty.cs} | 26 +-- src/SMAPI/Framework/Reflection/Reflector.cs | 106 +++++------ src/SMAPI/Framework/SContentManager.cs | 6 +- src/SMAPI/Framework/SGame.cs | 24 +-- src/SMAPI/IPrivateField.cs | 6 +- src/SMAPI/IPrivateMethod.cs | 6 +- src/SMAPI/IPrivateProperty.cs | 6 +- src/SMAPI/IReflectedField.cs | 26 +++ src/SMAPI/IReflectedMethod.cs | 27 +++ src/SMAPI/IReflectedProperty.cs | 26 +++ src/SMAPI/IReflectionHelper.cs | 55 +++++- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 9 +- 19 files changed, 357 insertions(+), 175 deletions(-) rename src/SMAPI/Framework/Reflection/{PrivateField.cs => ReflectedField.cs} (84%) rename src/SMAPI/Framework/Reflection/{PrivateMethod.cs => ReflectedMethod.cs} (83%) rename src/SMAPI/Framework/Reflection/{PrivateProperty.cs => ReflectedProperty.cs} (73%) create mode 100644 src/SMAPI/IReflectedField.cs create mode 100644 src/SMAPI/IReflectedMethod.cs create mode 100644 src/SMAPI/IReflectedProperty.cs diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 10c41d08..4508e641 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -57,14 +57,14 @@ namespace StardewModdingAPI.Framework.Content public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) { // init - this.Cache = reflection.GetPrivateField>(contentManager, "loadedAssets").GetValue(); + this.Cache = reflection.GetField>(contentManager, "loadedAssets").GetValue(); this.PossiblePathSeparators = possiblePathSeparators; this.PreferredPathSeparator = preferredPathSeparator; // get key normalisation logic if (Constants.TargetPlatform == Platform.Windows) { - IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); + IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); this.NormaliseAssetNameForPlatform = path => method.Invoke(path); } else diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index b07c6c7d..20bb0d2d 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework return; // build message - string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; + string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase} is deprecated since SMAPI {version})."; if (source == null) message += $"{Environment.NewLine}{Environment.StackTrace}"; diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 3709e05d..f81e05a9 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -125,7 +125,7 @@ namespace StardewModdingAPI.Framework #endif // get result - return reflection.GetPrivateField(Game1.spriteBatch, fieldName).GetValue(); + return reflection.GetField(Game1.spriteBatch, fieldName).GetValue(); } } } diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 8788b142..81453003 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod name for error messages. private readonly string ModName; + /// Manages deprecation warnings. + private readonly DeprecationManager DeprecationManager; + /********* ** Public methods @@ -25,15 +28,88 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The unique ID of the relevant mod. /// The mod name for error messages. /// The underlying reflection helper. - public ReflectionHelper(string modID, string modName, Reflector reflector) + /// Manages deprecation warnings. + public ReflectionHelper(string modID, string modName, Reflector reflector, DeprecationManager deprecationManager) : base(modID) { this.ModName = modName; this.Reflector = reflector; + this.DeprecationManager = deprecationManager; } + /// Get an instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField(obj, name, required) + ); + } + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField(type, name, required) + ); + } + + /// Get an instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty(obj, name, required) + ); + } + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty(type, name, required) + ); + } + + /// Get an instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(obj, name, required) + ); + } + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(type, name, required) + ); + } + + /**** - ** Fields + ** Obsolete ****/ /// Get a private instance field. /// The field type. @@ -41,11 +117,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The field name. /// Whether to throw an exception if the private field is not found. /// Returns the field wrapper, or null if the field doesn't exist and is false. + [Obsolete] public IPrivateField GetPrivateField(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateField(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateField)this.GetField(obj, name, required); } /// Get a private static field. @@ -53,26 +129,23 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The type which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete] public IPrivateField GetPrivateField(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateField(type, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateField)this.GetField(type, name, required); } - /**** - ** Properties - ****/ /// Get a private instance property. /// The property type. /// The object which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete] public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateProperty(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateProperty)this.GetProperty(obj, name, required); } /// Get a private static property. @@ -80,17 +153,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The type which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete] public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateProperty(type, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateProperty)this.GetProperty(type, name, required); } - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ /// Get the value of a private instance field. /// The field type. /// The object which has the field. @@ -101,9 +170,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /// This is a shortcut for followed by . /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. /// + [Obsolete] public TValue GetPrivateValue(object obj, string name, bool required = true) { - IPrivateField field = this.GetPrivateField(obj, name, required); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + IPrivateField field = (IPrivateField)this.GetField(obj, name, required); return field != null ? field.GetValue() : default(TValue); @@ -119,64 +190,36 @@ namespace StardewModdingAPI.Framework.ModHelpers /// This is a shortcut for followed by . /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. /// + [Obsolete] public TValue GetPrivateValue(Type type, string name, bool required = true) { - IPrivateField field = this.GetPrivateField(type, name, required); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + IPrivateField field = (IPrivateField)this.GetField(type, name, required); return field != null ? field.GetValue() : default(TValue); } - /**** - ** Methods - ****/ /// Get a private instance method. /// The object which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete] public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(obj, name, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateMethod)this.GetMethod(obj, name, required); } /// Get a private static method. /// The type which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete] public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(type, name, required) - ); - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required) - ); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - return this.AssertAccessAllowed( - this.Reflector.GetPrivateMethod(type, name, argumentTypes, required) - ); + this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); + return (IPrivateMethod)this.GetMethod(type, name, required); } @@ -187,7 +230,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The field value type. /// The field being accessed. /// Returns the same field instance for convenience. - private IPrivateField AssertAccessAllowed(IPrivateField field) + private IReflectedField AssertAccessAllowed(IReflectedField field) { this.AssertAccessAllowed(field?.FieldInfo); return field; @@ -197,7 +240,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The property value type. /// The property being accessed. /// Returns the same property instance for convenience. - private IPrivateProperty AssertAccessAllowed(IPrivateProperty property) + private IReflectedProperty AssertAccessAllowed(IReflectedProperty property) { this.AssertAccessAllowed(property?.PropertyInfo); return property; @@ -206,7 +249,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Assert that mods can use the reflection helper to access the given member. /// The method being accessed. /// Returns the same method instance for convenience. - private IPrivateMethod AssertAccessAllowed(IPrivateMethod method) + private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) { this.AssertAccessAllowed(method?.MethodInfo); return method; diff --git a/src/SMAPI/Framework/Reflection/PrivateField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs similarity index 84% rename from src/SMAPI/Framework/Reflection/PrivateField.cs rename to src/SMAPI/Framework/Reflection/ReflectedField.cs index 0bf45969..ad1557bb 100644 --- a/src/SMAPI/Framework/Reflection/PrivateField.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { - /// A private field obtained through reflection. + /// A field obtained through reflection. /// The field value type. - internal class PrivateField : IPrivateField + internal class ReflectedField : IPrivateField, IReflectedField { /********* ** Properties @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Reflection /// Whether the field is static. /// The or is null. /// The is null for a non-static field, or not null for a static field. - public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic) + public ReflectedField(Type parentType, object obj, FieldInfo field, bool isStatic) { // validate if (parentType == null) @@ -64,11 +64,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); + throw new InvalidCastException($"Can't convert the {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't get the value of the {this.DisplayName} field", ex); } } @@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); + throw new InvalidCastException($"Can't assign the {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't set the value of the {this.DisplayName} field", ex); } } } diff --git a/src/SMAPI/Framework/Reflection/PrivateMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs similarity index 83% rename from src/SMAPI/Framework/Reflection/PrivateMethod.cs rename to src/SMAPI/Framework/Reflection/ReflectedMethod.cs index ba2374f4..376de869 100644 --- a/src/SMAPI/Framework/Reflection/PrivateMethod.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs @@ -3,8 +3,8 @@ using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { - /// A private method obtained through reflection. - internal class PrivateMethod : IPrivateMethod + /// A method obtained through reflection. + internal class ReflectedMethod : IPrivateMethod, IReflectedMethod { /********* ** Properties @@ -33,10 +33,10 @@ namespace StardewModdingAPI.Framework.Reflection /// The type that has the method. /// The object that has the instance method(if applicable). /// The reflection metadata. - /// Whether the field is static. + /// Whether the method is static. /// The or is null. /// The is null for a non-static method, or not null for a static method. - public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic) + public ReflectedMethod(Type parentType, object obj, MethodInfo method, bool isStatic) { // validate if (parentType == null) @@ -67,7 +67,7 @@ namespace StardewModdingAPI.Framework.Reflection } catch (Exception ex) { - throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); } // cast return value @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); + throw new InvalidCastException($"Can't convert the return value of the {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); } } @@ -92,8 +92,8 @@ namespace StardewModdingAPI.Framework.Reflection } catch (Exception ex) { - throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex); + throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); } } } -} \ No newline at end of file +} diff --git a/src/SMAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs similarity index 73% rename from src/SMAPI/Framework/Reflection/PrivateProperty.cs rename to src/SMAPI/Framework/Reflection/ReflectedProperty.cs index c81f99c7..d6c964c1 100644 --- a/src/SMAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs @@ -3,9 +3,9 @@ using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { - /// A private property obtained through reflection. + /// A property obtained through reflection. /// The property value type. - internal class PrivateProperty : IPrivateProperty + internal class ReflectedProperty : IPrivateProperty, IReflectedProperty { /********* ** Properties @@ -31,13 +31,13 @@ namespace StardewModdingAPI.Framework.Reflection ** Public methods *********/ /// Construct an instance. - /// The type that has the field. - /// The object that has the instance field (if applicable). + /// The type that has the property. + /// The object that has the instance property (if applicable). /// The reflection metadata. - /// Whether the field is static. + /// Whether the property is static. /// The or is null. - /// The is null for a non-static field, or not null for a static field. - public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) + /// The is null for a non-static property, or not null for a static property. + public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) { // validate input if (parentType == null) @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Framework.Reflection public TValue GetValue() { if (this.GetMethod == null) - throw new InvalidOperationException($"The private {this.DisplayName} property has no get method."); + throw new InvalidOperationException($"The {this.DisplayName} property has no get method."); try { @@ -73,11 +73,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); + throw new InvalidCastException($"Can't convert the {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex); + throw new Exception($"Couldn't get the value of the {this.DisplayName} property", ex); } } @@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Reflection public void SetValue(TValue value) { if (this.SetMethod == null) - throw new InvalidOperationException($"The private {this.DisplayName} property has no set method."); + throw new InvalidOperationException($"The {this.DisplayName} property has no set method."); try { @@ -94,11 +94,11 @@ namespace StardewModdingAPI.Framework.Reflection } catch (InvalidCastException) { - throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); + throw new InvalidCastException($"Can't assign the {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); } catch (Exception ex) { - throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex); + throw new Exception($"Couldn't set the value of the {this.DisplayName} property", ex); } } } diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 23a48505..910e3a54 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -5,7 +5,7 @@ using System.Runtime.Caching; namespace StardewModdingAPI.Framework.Reflection { - /// Provides helper methods for accessing private game code. + /// Provides helper methods for accessing inaccessible code. /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). internal class Reflector { @@ -25,139 +25,139 @@ namespace StardewModdingAPI.Framework.Reflection /**** ** Fields ****/ - /// Get a private instance field. + /// Get a instance field. /// The field type. /// The object which has the field. /// The field name. - /// Whether to throw an exception if the private field is not found. + /// Whether to throw an exception if the field is not found. /// Returns the field wrapper, or null if the field doesn't exist and is false. - public IPrivateField GetPrivateField(object obj, string name, bool required = true) + public IReflectedField GetField(object obj, string name, bool required = true) { // validate if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object."); // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && field == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field."); return field; } - /// Get a private static field. + /// Get a static field. /// The field type. /// The type which has the field. /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + IReflectedField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); if (required && field == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field."); return field; } /**** ** Properties ****/ - /// Get a private instance property. + /// Get a instance property. /// The property type. /// The object which has the property. /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(object obj, string name, bool required = true) { // validate if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object."); // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && property == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property."); return property; } - /// Get a private static property. + /// Get a static property. /// The property type. /// The type which has the property. /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(Type type, string name, bool required = true) { // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + IReflectedProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && property == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property."); return property; } /**** ** Methods ****/ - /// Get a private instance method. + /// Get a instance method. /// The object which has the method. /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, bool required = true) { // validate if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object."); // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + IReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method."); return method; } - /// Get a private static method. + /// Get a static method. /// The type which has the method. /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, bool required = true) { // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + IReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method."); return method; } /**** ** Methods by signature ****/ - /// Get a private instance method. + /// Get a instance method. /// The object which has the method. /// The field name. /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, Type[] argumentTypes, bool required = true) { // validate parent if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object."); // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes); + ReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes); if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method with that signature."); return method; } - /// Get a private static method. + /// Get a static method. /// The type which has the method. /// The field name. /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, Type[] argumentTypes, bool required = true) { // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes); + ReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes); if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method with that signature."); return method; } @@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The object which has the field. /// The field name. /// The reflection binding which flags which indicates what type of field to find. - private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return field != null - ? new PrivateField(type, obj, field, isStatic) + ? new ReflectedField(type, obj, field, isStatic) : null; } @@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The object which has the property. /// The property name. /// The reflection binding which flags which indicates what type of property to find. - private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return property != null - ? new PrivateProperty(type, obj, property, isStatic) + ? new ReflectedProperty(type, obj, property, isStatic) : null; } @@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The object which has the method. /// The method name. /// The reflection binding which flags which indicates what type of method to find. - private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + private IReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => @@ -226,7 +226,7 @@ namespace StardewModdingAPI.Framework.Reflection }); return method != null - ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + ? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) : null; } @@ -236,7 +236,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The method name. /// The reflection binding which flags which indicates what type of method to find. /// The argument types of the method signature to find. - private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) + private ReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => @@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework.Reflection return methodInfo; }); return method != null - ? new PrivateMethod(type, obj, method, isStatic) + ? new ReflectedMethod(type, obj, method, isStatic) : null; } diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 524b2d17..1803098d 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework private readonly ContentCache Cache; /// The private method which generates the locale portion of an asset name. - private readonly IPrivateMethod GetKeyLocale; + private readonly IReflectedMethod GetKeyLocale; /// The language codes used in asset keys. private readonly IDictionary KeyLocales; @@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework // init this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); - this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode"); + this.GetKeyLocale = reflection.GetMethod(this, "languageCode"); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); // get asset data @@ -413,7 +413,7 @@ namespace StardewModdingAPI.Framework private IDictionary GetKeyLocales(Reflector reflection) { // get the private code field directly to avoid changed-code logic - IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + IReflectedField codeField = reflection.GetField(typeof(LocalizedContentManager), "_currentLangCode"); // remember previous settings LanguageCode previousCode = codeField.GetValue(); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 3062b0f6..e9777e0b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -133,20 +133,20 @@ namespace StardewModdingAPI.Framework // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. - private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); - private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); + private static List _fpsList => SGame.Reflection.GetField>(typeof(Game1), nameof(_fpsList)).GetValue(); + private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static float _fps { - set => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); + set => SGame.Reflection.GetField(typeof(Game1), nameof(_fps)).SetValue(value); } - private static Task _newDayTask => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); - public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); - private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(); - private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); + private static Task _newDayTask => SGame.Reflection.GetField(typeof(Game1), nameof(_newDayTask)).GetValue(); + private Color bgColor => SGame.Reflection.GetField(this, nameof(bgColor)).GetValue(); + public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop + public BlendState lightingBlend => SGame.Reflection.GetField(this, nameof(lightingBlend)).GetValue(); + private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); + private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke(); + private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); + private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming @@ -182,7 +182,7 @@ namespace StardewModdingAPI.Framework this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); - reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager + reflection.GetField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager } /**** diff --git a/src/SMAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs index 3e681c12..512bfdab 100644 --- a/src/SMAPI/IPrivateField.cs +++ b/src/SMAPI/IPrivateField.cs @@ -1,9 +1,11 @@ -using System.Reflection; +using System; +using System.Reflection; namespace StardewModdingAPI { /// A private field obtained through reflection. /// The field value type. + [Obsolete("Use " + nameof(IReflectedField) + " instead")] public interface IPrivateField { /********* @@ -23,4 +25,4 @@ namespace StardewModdingAPI //// The value to set. void SetValue(TValue value); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs index 67fc8b3c..b2fdaaeb 100644 --- a/src/SMAPI/IPrivateMethod.cs +++ b/src/SMAPI/IPrivateMethod.cs @@ -1,8 +1,10 @@ -using System.Reflection; +using System; +using System.Reflection; namespace StardewModdingAPI { /// A private method obtained through reflection. + [Obsolete("Use " + nameof(IReflectedMethod) + " instead")] public interface IPrivateMethod { /********* @@ -24,4 +26,4 @@ namespace StardewModdingAPI /// The method arguments to pass in. void Invoke(params object[] arguments); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs index 8d67fa7a..a24495dd 100644 --- a/src/SMAPI/IPrivateProperty.cs +++ b/src/SMAPI/IPrivateProperty.cs @@ -1,9 +1,11 @@ -using System.Reflection; +using System; +using System.Reflection; namespace StardewModdingAPI { /// A private property obtained through reflection. /// The property value type. + [Obsolete("Use " + nameof(IPrivateProperty) + " instead")] public interface IPrivateProperty { /********* @@ -23,4 +25,4 @@ namespace StardewModdingAPI //// The value to set. void SetValue(TValue value); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IReflectedField.cs b/src/SMAPI/IReflectedField.cs new file mode 100644 index 00000000..43ddad42 --- /dev/null +++ b/src/SMAPI/IReflectedField.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A field obtained through reflection. + /// The field value type. + public interface IReflectedField + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the field value. + TValue GetValue(); + + /// Set the field value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/SMAPI/IReflectedMethod.cs b/src/SMAPI/IReflectedMethod.cs new file mode 100644 index 00000000..de83b98c --- /dev/null +++ b/src/SMAPI/IReflectedMethod.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A method obtained through reflection. + public interface IReflectedMethod + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + TValue Invoke(params object[] arguments); + + /// Invoke the method. + /// The method arguments to pass in. + void Invoke(params object[] arguments); + } +} \ No newline at end of file diff --git a/src/SMAPI/IReflectedProperty.cs b/src/SMAPI/IReflectedProperty.cs new file mode 100644 index 00000000..73ad9f30 --- /dev/null +++ b/src/SMAPI/IReflectedProperty.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A property obtained through reflection. + /// The property value type. + public interface IReflectedProperty + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the property value. + TValue GetValue(); + + /// Set the property value. + //// The value to set. + void SetValue(TValue value); + } +} diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs index fb2c7861..fcebae42 100644 --- a/src/SMAPI/IReflectionHelper.cs +++ b/src/SMAPI/IReflectionHelper.cs @@ -1,18 +1,62 @@ -using System; +using System; namespace StardewModdingAPI { - /// Provides an API for accessing private game code. + /// Provides an API for accessing inaccessible code. public interface IReflectionHelper : IModLinked { /********* ** Public methods *********/ + /// Get an instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedField GetField(object obj, string name, bool required = true); + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedField GetField(Type type, string name, bool required = true); + + /// Get an instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + IReflectedProperty GetProperty(object obj, string name, bool required = true); + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + IReflectedProperty GetProperty(Type type, string name, bool required = true); + + /// Get an instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedMethod GetMethod(object obj, string name, bool required = true); + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedMethod GetMethod(Type type, string name, bool required = true); + + /***** + ** Obsolete + *****/ /// Get a private instance field. /// The field type. /// The object which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] IPrivateField GetPrivateField(object obj, string name, bool required = true); /// Get a private static field. @@ -20,6 +64,7 @@ namespace StardewModdingAPI /// The type which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")] IPrivateField GetPrivateField(Type type, string name, bool required = true); /// Get a private instance property. @@ -27,6 +72,7 @@ namespace StardewModdingAPI /// The object which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true); /// Get a private static property. @@ -34,6 +80,7 @@ namespace StardewModdingAPI /// The type which has the property. /// The property name. /// Whether to throw an exception if the private property is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true); /// Get the value of a private instance field. @@ -42,6 +89,7 @@ namespace StardewModdingAPI /// The field name. /// Whether to throw an exception if the private field is not found. /// This is a shortcut for followed by . + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] TValue GetPrivateValue(object obj, string name, bool required = true); /// Get the value of a private static field. @@ -50,18 +98,21 @@ namespace StardewModdingAPI /// The field name. /// Whether to throw an exception if the private field is not found. /// This is a shortcut for followed by . + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")] TValue GetPrivateValue(Type type, string name, bool required = true); /// Get a private instance method. /// The object which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); /// Get a private static method. /// The type which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. + [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3ba35e43..7bfb0abd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -728,7 +728,7 @@ namespace StardewModdingAPI IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 380ed733..0db94843 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -111,6 +111,9 @@ + + + @@ -169,7 +172,7 @@ - + @@ -198,8 +201,8 @@ - - + + From 6bdd49af13ed6bdfef8220c85ca32bca904d3a1e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 23:27:10 -0500 Subject: [PATCH 06/34] detect libgdiplus-missing exception and show a friendly error instead (#408) --- docs/release-notes.md | 1 + src/SMAPI/Framework/InternalExtensions.cs | 9 +++++++++ src/SMAPI/Framework/SContentManager.cs | 4 +++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index fa04c055..a76b5e19 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. * Fixed issue where a mod could change the cursor position reported to other mods. + * Improved cryptic libgdiplus errors on Mac when Mono isn't installed. ## 2.2 * For players: diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index f81e05a9..bec6c183 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -108,6 +108,15 @@ namespace StardewModdingAPI.Framework } } + /// Get the lowest exception in an exception stack. + /// The exception from which to search. + public static Exception GetInnermostException(this Exception exception) + { + while (exception.InnerException != null) + exception = exception.InnerException; + return exception; + } + /**** ** Sprite batch ****/ diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 1803098d..ebea6c84 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework return this.LoadImpl(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); } } From d3f6f9c70a0435a505d95a45b9bca2be2d71caaf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 23:42:22 -0500 Subject: [PATCH 07/34] fix log parser favicon (#405) --- docs/release-notes.md | 3 +++ src/SMAPI.Web/Startup.cs | 1 + 2 files changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a76b5e19..57d9c480 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,9 @@ * Fixed issue where a mod could change the cursor position reported to other mods. * Improved cryptic libgdiplus errors on Mac when Mono isn't installed. +* For the [log parser][]: + * Fixed broken favicon. + ## 2.2 * For players: * Fixed error when a mod loads custom assets on Linux/Mac. diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 16952124..2f2b0d11 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -89,6 +89,7 @@ namespace StardewModdingAPI.Web req.Host.Host != "localhost" && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.")) && !req.Path.StartsWithSegments("/content") + && !req.Path.StartsWithSegments("/favicon.ico") )) // shortcut redirects From 69c9ab0ecd184e4706a8e6394b38fa592cb808d0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 21:29:39 -0500 Subject: [PATCH 08/34] trace mods with no update keys, tweak update-check logging --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 174 ++++++++++++++++++++++-------------------- 2 files changed, 91 insertions(+), 84 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 57d9c480..0e2477f4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.3 * For modders: * Added `IsSuppressed` to input events so mods can optionally avoid handling a key another mod already handled. + * Added trace message listing mods with no update keys. * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. * Fixed issue where a mod could change the cursor position reported to other mods. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7bfb0abd..8bc2c675 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -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,94 +526,101 @@ 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 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); + } + + // 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 updatesByMod = new Dictionary(); + 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; + } + + // 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; + } + } + + // output + if (updatesByMod.Any()) + { + 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); } } - - // prepare update keys - Dictionary 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 + 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() ); - - // 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 updatesByMod = new Dictionary(); - 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; - } - - // 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) - { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) - updatesByMod[mod] = info; - } } - - // output - if (updatesByMod.Any()) - { - 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); - } - } - 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(); } From 971aff8368a8a2c196d942984926efc2f80cc216 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 22:29:56 -0500 Subject: [PATCH 09/34] generalise internal mod registry (#409) --- src/SMAPI/Framework/DeprecationManager.cs | 4 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 9 +-- src/SMAPI/Framework/ModRegistry.cs | 61 +++++++------------ src/SMAPI/Program.cs | 10 +-- 4 files changed, 33 insertions(+), 51 deletions(-) diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index 20bb0d2d..7a824a05 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework /// How deprecated the code is. 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); } /// Log a deprecation warning. @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. public bool MarkWarned(string nounPhrase, string version) { - return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version); + return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version); } /// Mark a deprecation warning as already logged. diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9e824694..4e3f56de 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; namespace StardewModdingAPI.Framework.ModHelpers { @@ -27,7 +28,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Get metadata for all loaded mods. public IEnumerable GetAll() { - return this.Registry.GetAll(); + return this.Registry.GetAll().Select(p => p.Manifest); } /// Get metadata for a loaded mod. @@ -35,14 +36,14 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Returns the matching mod's metadata, or null if not found. public IManifest Get(string uniqueID) { - return this.Registry.Get(uniqueID); + return this.Registry.Get(uniqueID)?.Manifest; } /// Get whether a mod has been loaded. /// The mod's unique ID. public bool IsLoaded(string uniqueID) { - return this.Registry.IsLoaded(uniqueID); + return this.Registry.Get(uniqueID) != null; } } } diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 9dde7a20..4dbc3541 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -15,26 +15,31 @@ namespace StardewModdingAPI.Framework /// The registered mod data. private readonly List Mods = new List(); - /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). - private readonly IDictionary ModNamesByAssembly = new Dictionary(); + /// An assembly full name => mod lookup. + private readonly IDictionary ModNamesByAssembly = new Dictionary(); /********* ** Public methods *********/ - /**** - ** Basic metadata - ****/ - /// Get metadata for all loaded mods. - public IEnumerable GetAll() + /// Register a mod as a possible source of deprecation warnings. + /// The mod metadata. + public void Add(IModMetadata metadata) { - return this.Mods.Select(p => p.Manifest); + this.Mods.Add(metadata); + this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; + } + + /// Get metadata for all loaded mods. + public IEnumerable GetAll() + { + return this.Mods.Select(p => p); } /// Get metadata for a loaded mod. /// The mod's unique ID. /// Returns the matching mod's metadata, or null if not found. - public IManifest Get(string uniqueID) + public IModMetadata Get(string uniqueID) { // normalise search ID if (string.IsNullOrWhiteSpace(uniqueID)) @@ -42,37 +47,13 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); } - /// Get whether a mod has been loaded. - /// The mod's unique ID. - public bool IsLoaded(string uniqueID) - { - return this.Get(uniqueID) != null; - } - - /**** - ** Mod data - ****/ - /// Register a mod as a possible source of deprecation warnings. - /// The mod metadata. - public void Add(IModMetadata metadata) - { - this.Mods.Add(metadata); - this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName; - } - - /// Get all enabled mods. - public IEnumerable GetMods() - { - return (from mod in this.Mods select mod); - } - - /// Get the friendly mod name which defines a type. + /// Get the mod metadata from one of its assemblies. /// The type to check. /// Returns the mod name, or null if the type isn't part of a known mod. - public string GetModFrom(Type type) + public IModMetadata GetFrom(Type type) { // null if (type == null) @@ -89,7 +70,7 @@ namespace StardewModdingAPI.Framework /// Get the friendly name for the closest assembly registered as a source of deprecation warnings. /// Returns the source name, or null if no registered assemblies were found. - public string GetModFromStack() + public IModMetadata GetFromStack() { // get stack frames StackTrace stack = new StackTrace(); @@ -101,9 +82,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/Program.cs b/src/SMAPI/Program.cs index 8bc2c675..bd4692e6 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); } @@ -753,7 +753,7 @@ namespace StardewModdingAPI } } } - IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray(); // log skipped mods this.Monitor.Newline(); @@ -858,7 +858,7 @@ namespace StardewModdingAPI 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> translations = new Dictionary>(); From 2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 23:33:10 -0500 Subject: [PATCH 10/34] add prototype of mod-provided APIs (#409) --- src/SMAPI/Framework/IModMetadata.cs | 6 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 6 + src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 +- src/SMAPI/IModProvidedApi.cs | 6 + src/SMAPI/IModRegistry.cs | 8 +- src/SMAPI/Program.cs | 152 +++++++++++++----- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 145 insertions(+), 41 deletions(-) create mode 100644 src/SMAPI/IModProvidedApi.cs diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index c21734a7..c4be7daf 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework /// The mod instance (if it was loaded). IMod Mod { get; } + /// The mod-provided API (if any). + IModProvidedApi Api { get; } + /********* ** Public methods @@ -42,6 +45,7 @@ namespace StardewModdingAPI.Framework /// Set the mod instance. /// The mod instance to set. - IModMetadata SetMod(IMod mod); + /// The mod-provided API (if any). + IModMetadata SetMod(IMod mod, IModProvidedApi api); } } diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 4e3f56de..340205f3 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -45,5 +45,11 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.Registry.Get(uniqueID) != null; } + + /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. + public IModProvidedApi GetApi(string uniqueID) + { + return this.Registry.Get(uniqueID)?.Api; + } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 5055da75..2e5c27be 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod instance (if it was loaded). public IMod Mod { get; private set; } + /// The mod-provided API (if any). + public IModProvidedApi Api { get; private set; } + /********* ** Public methods @@ -59,9 +62,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Set the mod instance. /// The mod instance to set. - public IModMetadata SetMod(IMod mod) + /// The mod-provided API (if any). + public IModMetadata SetMod(IMod mod, IModProvidedApi api) { this.Mod = mod; + this.Api = api; return this; } } diff --git a/src/SMAPI/IModProvidedApi.cs b/src/SMAPI/IModProvidedApi.cs new file mode 100644 index 00000000..9884ca78 --- /dev/null +++ b/src/SMAPI/IModProvidedApi.cs @@ -0,0 +1,6 @@ +namespace StardewModdingAPI +{ + /// An API provided by a mod for other mods to use. + /// This is a marker interface. Each mod can only have one implementation of . + public interface IModProvidedApi { } +} diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index 5ef3fd65..fd71d72a 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,9 @@ namespace StardewModdingAPI /// Get whether a mod has been loaded. /// The mod's unique ID. bool IsLoaded(string uniqueID); + + /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. + /// The mod's unique ID. + IModProvidedApi GetApi(string uniqueID); } -} \ No newline at end of file +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index bd4692e6..6330cc1a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -696,55 +696,34 @@ 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) + // init mod helpers + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IModHelper modHelper; { - TrackSkip(metadata, "its entry class couldn't be instantiated."); - continue; - } - - // inject data - { - 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, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); 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 instances + if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) + continue; + if (this.TryLoadModProvidedApi(modAssembly, modHelper, monitor, error => this.Monitor.Log($"Failed loading {metadata.DisplayName}'s mod-provided API. Integrations may not work correctly. Error: {error}", LogLevel.Warn), out IModProvidedApi api)) + this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); + + // init mod + mod.ModManifest = manifest; + mod.Helper = modHelper; + mod.Monitor = monitor; + // track mod - metadata.SetMod(mod); + metadata.SetMod(mod, api); this.ModRegistry.Add(metadata); } catch (Exception ex) @@ -854,6 +833,105 @@ namespace StardewModdingAPI } } + /// Load a mod's entry class. + /// The mod assembly. + /// A callback invoked when loading fails. + /// The loaded instance. + private bool TryLoadModEntry(Assembly modAssembly, Action 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; + } + + /// Load a mod's implementation. + /// The mod assembly. + /// The mod's helper instance. + /// The mod's monitor instance. + /// A callback invoked when loading fails. + /// The loaded instance. + private bool TryLoadModProvidedApi(Assembly modAssembly, IModHelper modHelper, IMonitor monitor, Action onError, out IModProvidedApi api) + { + api = null; + + // find type + TypeInfo[] apis = modAssembly.DefinedTypes.Where(type => typeof(IModProvidedApi).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); + if (apis.Length == 0) + return false; + if (apis.Length > 1) + { + onError($"its DLL contains multiple '{nameof(IModProvidedApi)}' implementations."); + return false; + } + + // get constructor + ConstructorInfo constructor = ( + from constr in apis[0].GetConstructors() + let args = constr.GetParameters() + where + !args.Any() + || args.All(arg => typeof(IModHelper).IsAssignableFrom(arg.ParameterType) || typeof(IMonitor).IsAssignableFrom(arg.ParameterType)) + orderby args.Length descending + select constr + ).FirstOrDefault(); + if (constructor == null) + { + onError($"its {nameof(IModProvidedApi)} must have a constructor with zero arguments, or only arguments of type {nameof(IModHelper)} or {nameof(IMonitor)}."); + return false; + } + + // construct instance + try + { + // prepare constructor args + ParameterInfo[] args = constructor.GetParameters(); + object[] values = new object[args.Length]; + for (int i = 0; i < args.Length; i++) + { + if (typeof(IModHelper).IsAssignableFrom(args[i].ParameterType)) + values[i] = modHelper; + else if (typeof(IMonitor).IsAssignableFrom(args[i].ParameterType)) + values[i] = monitor; + else + { + // shouldn't happen + onError($"its {nameof(IModProvidedApi)} instance's constructor has unexpected argument type {args[i].ParameterType.FullName}."); + return false; + } + } + + // instantiate + api = (IModProvidedApi)constructor.Invoke(values); + return true; + } + catch (Exception ex) + { + onError($"its {nameof(IModProvidedApi)} couldn't be constructed: {ex.GetLogSummary()}"); + return false; + } + } + /// Reload translations for all mods. private void ReloadTranslations() { diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 0db94843..579ed487 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -114,6 +114,7 @@ + From 7d644aeabee63c0d51d4e89360d2fdab0e51b8be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 00:09:28 -0500 Subject: [PATCH 11/34] switch to simpler approach for mod-provided APIs (#409) --- src/SMAPI/Framework/IModMetadata.cs | 9 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 2 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 12 ++- src/SMAPI/IMod.cs | 7 +- src/SMAPI/IModProvidedApi.cs | 6 -- src/SMAPI/IModRegistry.cs | 2 +- src/SMAPI/Mod.cs | 3 + src/SMAPI/Program.cs | 86 ++++--------------- src/SMAPI/StardewModdingAPI.csproj | 1 - 9 files changed, 40 insertions(+), 88 deletions(-) delete mode 100644 src/SMAPI/IModProvidedApi.cs diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index c4be7daf..a36994fd 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Framework IMod Mod { get; } /// The mod-provided API (if any). - IModProvidedApi Api { get; } + object Api { get; } /********* @@ -45,7 +45,10 @@ namespace StardewModdingAPI.Framework /// Set the mod instance. /// The mod instance to set. - /// The mod-provided API (if any). - IModMetadata SetMod(IMod mod, IModProvidedApi api); + IModMetadata SetMod(IMod mod); + + /// Set the mod-provided API instance. + /// The mod-provided API. + IModMetadata SetApi(object api); } } diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 340205f3..949d986a 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. - public IModProvidedApi GetApi(string uniqueID) + public object GetApi(string uniqueID) { return this.Registry.Get(uniqueID)?.Api; } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 2e5c27be..30fe211b 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IMod Mod { get; private set; } /// The mod-provided API (if any). - public IModProvidedApi Api { get; private set; } + public object Api { get; private set; } /********* @@ -62,10 +62,16 @@ namespace StardewModdingAPI.Framework.ModLoading /// Set the mod instance. /// The mod instance to set. - /// The mod-provided API (if any). - public IModMetadata SetMod(IMod mod, IModProvidedApi api) + public IModMetadata SetMod(IMod mod) { this.Mod = mod; + return this; + } + + /// Set the mod-provided API instance. + /// The mod-provided API. + public IModMetadata SetApi(object api) + { this.Api = api; return this; } 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 { /// The implementation for a Stardew Valley mod. public interface IMod @@ -22,5 +22,8 @@ /// The mod entry point, called after the mod is first loaded. /// Provides simplified APIs for writing mods. void Entry(IModHelper helper); + + /// Get an API that other mods can access. This is always called after . + object GetApi(); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IModProvidedApi.cs b/src/SMAPI/IModProvidedApi.cs deleted file mode 100644 index 9884ca78..00000000 --- a/src/SMAPI/IModProvidedApi.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StardewModdingAPI -{ - /// An API provided by a mod for other mods to use. - /// This is a marker interface. Each mod can only have one implementation of . - public interface IModProvidedApi { } -} diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index fd71d72a..f84cfcfb 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -19,6 +19,6 @@ namespace StardewModdingAPI /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. /// The mod's unique ID. - IModProvidedApi GetApi(string uniqueID); + object GetApi(string uniqueID); } } 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 /// Provides simplified APIs for writing mods. public abstract void Entry(IModHelper helper); + /// Get an API that other mods can access. This is always called after . + public virtual object GetApi() => null; + /// Release or reset unmanaged resources. public void Dispose() { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6330cc1a..e7552630 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -711,11 +711,9 @@ namespace StardewModdingAPI modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); } - // get mod instances + // get mod instance if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod)) continue; - if (this.TryLoadModProvidedApi(modAssembly, modHelper, monitor, error => this.Monitor.Log($"Failed loading {metadata.DisplayName}'s mod-provided API. Integrations may not work correctly. Error: {error}", LogLevel.Warn), out IModProvidedApi api)) - this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); // init mod mod.ModManifest = manifest; @@ -723,7 +721,7 @@ namespace StardewModdingAPI mod.Monitor = monitor; // track mod - metadata.SetMod(mod, api); + metadata.SetMod(mod); this.ModRegistry.Add(metadata); } catch (Exception ex) @@ -796,6 +794,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 @@ -865,73 +876,6 @@ namespace StardewModdingAPI return true; } - /// Load a mod's implementation. - /// The mod assembly. - /// The mod's helper instance. - /// The mod's monitor instance. - /// A callback invoked when loading fails. - /// The loaded instance. - private bool TryLoadModProvidedApi(Assembly modAssembly, IModHelper modHelper, IMonitor monitor, Action onError, out IModProvidedApi api) - { - api = null; - - // find type - TypeInfo[] apis = modAssembly.DefinedTypes.Where(type => typeof(IModProvidedApi).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); - if (apis.Length == 0) - return false; - if (apis.Length > 1) - { - onError($"its DLL contains multiple '{nameof(IModProvidedApi)}' implementations."); - return false; - } - - // get constructor - ConstructorInfo constructor = ( - from constr in apis[0].GetConstructors() - let args = constr.GetParameters() - where - !args.Any() - || args.All(arg => typeof(IModHelper).IsAssignableFrom(arg.ParameterType) || typeof(IMonitor).IsAssignableFrom(arg.ParameterType)) - orderby args.Length descending - select constr - ).FirstOrDefault(); - if (constructor == null) - { - onError($"its {nameof(IModProvidedApi)} must have a constructor with zero arguments, or only arguments of type {nameof(IModHelper)} or {nameof(IMonitor)}."); - return false; - } - - // construct instance - try - { - // prepare constructor args - ParameterInfo[] args = constructor.GetParameters(); - object[] values = new object[args.Length]; - for (int i = 0; i < args.Length; i++) - { - if (typeof(IModHelper).IsAssignableFrom(args[i].ParameterType)) - values[i] = modHelper; - else if (typeof(IMonitor).IsAssignableFrom(args[i].ParameterType)) - values[i] = monitor; - else - { - // shouldn't happen - onError($"its {nameof(IModProvidedApi)} instance's constructor has unexpected argument type {args[i].ParameterType.FullName}."); - return false; - } - } - - // instantiate - api = (IModProvidedApi)constructor.Invoke(values); - return true; - } - catch (Exception ex) - { - onError($"its {nameof(IModProvidedApi)} couldn't be constructed: {ex.GetLogSummary()}"); - return false; - } - } - /// Reload translations for all mods. private void ReloadTranslations() { diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 579ed487..0db94843 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -114,7 +114,6 @@ - From d04cacbdd0729140e4d8e93323ba66ee90ff9d2a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 00:16:34 -0500 Subject: [PATCH 12/34] log mod-provided API access (#409) --- .../Framework/ModHelpers/ModRegistryHelper.cs | 15 +++++++++++++-- src/SMAPI/Program.cs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 949d986a..827c77d5 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -12,6 +12,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The underlying mod registry. private readonly ModRegistry Registry; + /// Encapsulates monitoring and logging for the mod. + private readonly IMonitor Monitor; + + /// The mod IDs for APIs accessed by this instanced. + private readonly HashSet AccessedModApis = new HashSet(); + /********* ** Public methods @@ -19,10 +25,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The unique ID of the relevant mod. /// The underlying mod registry. - public ModRegistryHelper(string modID, ModRegistry registry) + /// Encapsulates monitoring and logging for the mod. + public ModRegistryHelper(string modID, ModRegistry registry, IMonitor monitor) : base(modID) { this.Registry = registry; + this.Monitor = monitor; } /// Get metadata for all loaded mods. @@ -49,7 +57,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. public object GetApi(string uniqueID) { - return this.Registry.Get(uniqueID)?.Api; + 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; } } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e7552630..17fe2f36 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -706,7 +706,7 @@ namespace StardewModdingAPI 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, this.DeprecationManager); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, monitor); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); } From 0e43041777d68b96f110fa38ad7424b855db761a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:00:32 -0500 Subject: [PATCH 13/34] add support for casting mod-provided API to an interface without a direct assembly reference (#409) --- build/common.targets | 1 + build/prepare-install-package.targets | 2 ++ .../Framework/ModHelpers/ModRegistryHelper.cs | 24 +++++++++++++++++++ src/SMAPI/IModRegistry.cs | 5 ++++ src/SMAPI/StardewModdingAPI.csproj | 3 +++ src/SMAPI/packages.config | 1 + 6 files changed, 36 insertions(+) diff --git a/build/common.targets b/build/common.targets index aa11344e..15c935e3 100644 --- a/build/common.targets +++ b/build/common.targets @@ -86,6 +86,7 @@ + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index f2a2b23c..e88ca273 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -22,6 +22,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 827c77d5..68201d9a 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using ImpromptuInterface; namespace StardewModdingAPI.Framework.ModHelpers { @@ -62,5 +63,28 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace); return mod?.Api; } + + /// 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 null. + /// The interface which matches the properties and methods you intend to access. + /// The mod's unique ID. + public TInterface GetApi(string uniqueID) where TInterface : class + { + // validate + if (!typeof(TInterface).IsInterface) + { + this.Monitor.Log("Tried to map a mod-provided API into a class; must be an interface."); + 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 api.ActLike(); + } } } diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index f84cfcfb..a06e099e 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -20,5 +20,10 @@ namespace StardewModdingAPI /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. /// The mod's unique ID. object GetApi(string uniqueID); + + /// 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 null. + /// The interface which matches the properties and methods you intend to access. + /// The mod's unique ID. + TInterface GetApi(string uniqueID) where TInterface : class; } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 0db94843..c585a0c4 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -53,6 +53,9 @@ icon.ico + + ..\packages\ImpromptuInterface.6.2.2\lib\net40\ImpromptuInterface.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 98d742c7..60be6881 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file From 59a25a12ffdb90c5a9a3db90be933ca4e76eb64f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:09:43 -0500 Subject: [PATCH 14/34] validate interface is public (#409) --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 68201d9a..d39c885c 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -72,7 +72,12 @@ namespace StardewModdingAPI.Framework.ModHelpers // validate if (!typeof(TInterface).IsInterface) { - this.Monitor.Log("Tried to map a mod-provided API into a class; must be an interface."); + 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; } From e00424068f3da7c4f91187872e96c90fa61e47db Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:33:11 -0500 Subject: [PATCH 15/34] block access to mod-provided APIs until all mods are initialised (#409) --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 5 +++++ src/SMAPI/Framework/ModRegistry.cs | 3 +++ src/SMAPI/Program.cs | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index d39c885c..9574a632 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -70,6 +70,11 @@ namespace StardewModdingAPI.Framework.ModHelpers public TInterface GetApi(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); diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 4dbc3541..453d2868 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -18,6 +18,9 @@ namespace StardewModdingAPI.Framework /// An assembly full name => mod lookup. private readonly IDictionary ModNamesByAssembly = new Dictionary(); + /// Whether all mods have been initialised and their method called. + public bool AreAllModsInitialised { get; set; } + /********* ** Public methods diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 17fe2f36..786549fe 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -842,6 +842,9 @@ 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; } /// Load a mod's entry class. From ef23043e1f63c4c910cc59497d6244e3727c92f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:56:32 -0500 Subject: [PATCH 16/34] reintroduce GameEvents.FirstUpdateTick to simplify mod integrations (#409) --- src/SMAPI/Events/GameEvents.cs | 10 ++++++++++ src/SMAPI/Framework/SGame.cs | 13 ++++++++----- src/SMAPI/Metadata/InstructionMetadata.cs | 1 - 3 files changed, 18 insertions(+), 6 deletions(-) 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 /// Raised every 60th tick (≈once per second). public static event EventHandler OneSecondTick; + /// Raised once after the game initialises and all methods have been called. + public static event EventHandler FirstUpdateTick; + /********* ** Internal methods @@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeFirstUpdateTick(IMonitor monitor) + { + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList()); + } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index e9777e0b..0a614f17 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -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) @@ -725,7 +728,7 @@ namespace StardewModdingAPI.Framework } if (Game1.overlayMenu != null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } @@ -759,7 +762,7 @@ namespace StardewModdingAPI.Framework } if (Game1.overlayMenu != null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } @@ -793,7 +796,7 @@ namespace StardewModdingAPI.Framework } if (Game1.overlayMenu != null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } @@ -826,7 +829,7 @@ namespace StardewModdingAPI.Framework } if (Game1.overlayMenu != null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } 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), From a391dfe26372714a395abc5ee77b603e29562691 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:58:42 -0500 Subject: [PATCH 17/34] update release notes (#409) --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0e2477f4..8407455c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,8 @@ # Release notes ## 2.3 * For modders: + * **Added mod-provided APIs** which enable simple integrations between mods, even without direct assembly references. + * Added `GameEvents.FirstUpdateTick` event, which is called once after all mods are initialised. * Added `IsSuppressed` to input events so mods can optionally avoid handling a key another mod already handled. * Added trace message listing mods with no update keys. * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. From 356503325f4ef5ed84c359d4b56991c2a7f18b8e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 18:44:38 -0500 Subject: [PATCH 18/34] fix compatibility on Mac (#409) --- build/prepare-install-package.targets | 1 + src/SMAPI/StardewModdingAPI.csproj | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index e88ca273..f8262271 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -24,6 +24,7 @@ + diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c585a0c4..026ac106 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -84,7 +84,10 @@ - + + + True + From 21fd2d1e39a6a94758f6298c2da52cd46cffdfcd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Dec 2017 21:37:08 -0500 Subject: [PATCH 19/34] emit proxy classes directly to simplify crossplatform compatibility (#409) --- build/common.targets | 1 - build/prepare-install-package.targets | 2 - .../Framework/ModHelpers/ModRegistryHelper.cs | 11 +- .../Reflection/InterfaceProxyBuilder.cs | 138 ++++++++++++++++++ src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 4 +- src/SMAPI/packages.config | 1 - 7 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs diff --git a/build/common.targets b/build/common.targets index 15c935e3..aa11344e 100644 --- a/build/common.targets +++ b/build/common.targets @@ -86,7 +86,6 @@ - diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index f8262271..dde2ff0a 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -22,7 +22,6 @@ - @@ -38,7 +37,6 @@ - diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9574a632..ea0dbb38 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using ImpromptuInterface; +using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers { @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The mod IDs for APIs accessed by this instanced. private readonly HashSet AccessedModApis = new HashSet(); + /// Generates proxy classes to access mod APIs through an arbitrary interface. + private readonly InterfaceProxyBuilder ProxyBuilder; + /********* ** Public methods @@ -26,11 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The unique ID of the relevant mod. /// The underlying mod registry. + /// Generates proxy classes to access mod APIs through an arbitrary interface. /// Encapsulates monitoring and logging for the mod. - public ModRegistryHelper(string modID, ModRegistry registry, IMonitor monitor) + public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor) : base(modID) { this.Registry = registry; + this.ProxyBuilder = proxyBuilder; this.Monitor = monitor; } @@ -94,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get API of type if (api is TInterface castApi) return castApi; - return api.ActLike(); + return this.ProxyBuilder.CreateProxy(api, this.ModID, uniqueID); } } } 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 +{ + /// Generates proxy classes to access mod APIs through an arbitrary interface. + internal class InterfaceProxyBuilder + { + /********* + ** Properties + *********/ + /// The CLR module in which to create proxy classes. + private readonly ModuleBuilder ModuleBuilder; + + /// The generated proxy types. + private readonly IDictionary GeneratedTypes = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + 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"); + } + + /// Create an API proxy. + /// The interface through which to access the API. + /// The API instance to access. + /// The unique ID of the mod consuming the API. + /// The unique ID of the mod providing the API. + public TInterface CreateProxy(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 + *********/ + /// Define a class which proxies access to a target type through an interface. + /// The name of the proxy type to generate. + /// The interface type through which to access the target. + /// The target type to access. + 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(); + } + + /// Define a method which proxies access to a method on the target. + /// The proxy type being generated. + /// The target method. + /// The proxy field containing the API instance. + 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/Program.cs b/src/SMAPI/Program.cs index 786549fe..7eda9c66 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -655,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 @@ -706,7 +707,7 @@ namespace StardewModdingAPI 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, this.DeprecationManager); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, monitor); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 026ac106..0e8ccaa3 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -53,9 +53,6 @@ icon.ico - - ..\packages\ImpromptuInterface.6.2.2\lib\net40\ImpromptuInterface.dll - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True @@ -116,6 +113,7 @@ + diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 60be6881..98d742c7 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,6 +1,5 @@  - \ No newline at end of file From 4c3ae950c6be0840e98d1328eeca9effca9c3807 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Dec 2017 23:28:16 -0500 Subject: [PATCH 20/34] rm unneeded assembly references (#409) --- build/prepare-install-package.targets | 1 - .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 4 ---- src/SMAPI/StardewModdingAPI.csproj | 7 ------- 3 files changed, 12 deletions(-) diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index dde2ff0a..f2a2b23c 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -23,7 +23,6 @@ - diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index f228bb25..a65ad72c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -41,10 +41,6 @@ False - - - - diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 0e8ccaa3..f76ac439 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,13 +79,6 @@ True - - - - - True - - From 9cb5db2d33d19fe3d19c2516e0b5c86d5eec87bb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Dec 2017 12:59:46 -0500 Subject: [PATCH 21/34] bump version for upcoming release --- build/GlobalAssemblyInfo.cs | 4 ++-- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs index 65211aad..bcfdc124 100644 --- a/build/GlobalAssemblyInfo.cs +++ b/build/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Reflection; using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("2.2.0.0")] -[assembly: AssemblyFileVersion("2.2.0.0")] +[assembly: AssemblyVersion("2.3.0.0")] +[assembly: AssemblyFileVersion("2.3.0.0")] diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 664dfabf..06ab1b54 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -3,7 +3,7 @@ "Author": "SMAPI", "Version": { "MajorVersion": 2, - "MinorVersion": 0, + "MinorVersion": 3, "PatchVersion": 0, "Build": null }, 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 ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.2"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.3"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); From 3fe2f586b1bf3bc138039eb01650fe8a92b7d235 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Dec 2017 17:36:05 -0500 Subject: [PATCH 22/34] avoid potential issue in SMAPI's uninstall paths --- src/SMAPI.Installer/InteractiveInstaller.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 83f353ae..43d31eea 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -97,8 +97,8 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.pdb"); // obsolete - yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 - yield return GetInstallPath("Mods/TrainerMod"); // *–2.0 (renamed to ConsoleCommands) + yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 + yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 if (modsDir.Exists) From c3d0ce7245b76d26ea22ceb5430ee526d43170f7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Dec 2017 17:41:16 -0500 Subject: [PATCH 23/34] mark TrainerMod as obsolete (replaced by ConsoleCommands) --- docs/release-notes.md | 1 + src/SMAPI/StardewModdingAPI.config.json | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8407455c..c11ee46f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. * Fixed issue where a mod could change the cursor position reported to other mods. + * Updated compatibility list. * Improved cryptic libgdiplus errors on Mac when Mono isn't installed. * For the [log parser][]: 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 @@ -1890,6 +1890,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 "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", From 05541c11a72735d79d98cf3ae14d592e70bd8f54 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Dec 2017 23:28:07 -0500 Subject: [PATCH 24/34] decouple API clients from mods API (#411) --- .../Controllers/ModsApiController.cs | 34 +++----- .../Clients/Chucklefish/ChucklefishClient.cs | 83 +++++++++++++++++++ .../Clients/Chucklefish/ChucklefishMod.cs | 18 ++++ .../Clients/Chucklefish/IChucklefishClient.cs | 17 ++++ .../Framework/Clients/GitHub/GitHubClient.cs | 70 ++++++++++++++++ .../Framework/Clients/GitHub/GitRelease.cs | 19 +++++ .../Framework/Clients/GitHub/IGitHubClient.cs | 17 ++++ .../Framework/Clients/Nexus/INexusClient.cs | 17 ++++ .../Framework/Clients/Nexus/NexusClient.cs | 48 +++++++++++ .../Framework/Clients/Nexus/NexusMod.cs | 21 +++++ .../ConfigModels/ApiClientsConfig.cs | 56 +++++++++++++ .../ConfigModels/ModUpdateCheckConfig.cs | 49 ----------- .../ModRepositories/ChucklefishRepository.cs | 50 ++--------- .../ModRepositories/GitHubRepository.cs | 59 +++---------- .../ModRepositories/NexusRepository.cs | 51 +++--------- src/SMAPI.Web/Startup.cs | 30 +++++++ src/SMAPI.Web/appsettings.Development.json | 2 +- src/SMAPI.Web/appsettings.json | 19 +++-- 18 files changed, 447 insertions(+), 213 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index a600662c..dcb4ec52 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.Clients.Chucklefish; +using StardewModdingAPI.Web.Framework.Clients.GitHub; +using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; @@ -39,39 +42,22 @@ namespace StardewModdingAPI.Web.Controllers /// Construct an instance. /// The cache in which to store mod metadata. /// The config settings for mod update checks. - public ModsApiController(IMemoryCache cache, IOptions configProvider) + /// The Chucklefish API client. + /// The GitHub API client. + /// The Nexus API client. + public ModsApiController(IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus) { ModUpdateCheckConfig config = configProvider.Value; this.Cache = cache; this.CacheMinutes = config.CacheMinutes; this.VersionRegex = config.SemanticVersionRegex; - - string version = this.GetType().Assembly.GetName().Version.ToString(3); this.Repositories = new IModRepository[] { - new ChucklefishRepository( - vendorKey: config.ChucklefishKey, - userAgent: string.Format(config.ChucklefishUserAgent, version), - baseUrl: config.ChucklefishBaseUrl, - modPageUrlFormat: config.ChucklefishModPageUrlFormat - ), - new GitHubRepository( - vendorKey: config.GitHubKey, - baseUrl: config.GitHubBaseUrl, - releaseUrlFormat: config.GitHubReleaseUrlFormat, - userAgent: string.Format(config.GitHubUserAgent, version), - acceptHeader: config.GitHubAcceptHeader, - username: config.GitHubUsername, - password: config.GitHubPassword - ), - new NexusRepository( - vendorKey: config.NexusKey, - userAgent: config.NexusUserAgent, - baseUrl: config.NexusBaseUrl, - modUrlFormat: config.NexusModUrlFormat - ) + new ChucklefishRepository(config.ChucklefishKey, chucklefish), + new GitHubRepository(config.GitHubKey, github), + new NexusRepository(config.NexusKey, nexus) } .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); } diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs new file mode 100644 index 00000000..6772672c --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -0,0 +1,83 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish +{ + /// An HTTP client for fetching mod metadata from the Chucklefish mod site. + internal class ChucklefishClient : IChucklefishClient + { + /********* + ** Properties + *********/ + /// The base URL for the Chucklefish mod site. + private readonly string BaseUrl; + + /// The URL for a mod page excluding the base URL, where {0} is the mod ID. + private readonly string ModPageUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the API client. + /// The base URL for the Chucklefish mod site. + /// The URL for a mod page excluding the , where {0} is the mod ID. + public ChucklefishClient(string userAgent, string baseUrl, string modPageUrlFormat) + { + this.BaseUrl = baseUrl; + this.ModPageUrlFormat = modPageUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod. + /// The Chucklefish mod ID. + /// Returns the mod info if found, else null. + public async Task GetModAsync(uint id) + { + // fetch HTML + string html; + try + { + html = await this.Client + .GetAsync(string.Format(this.ModPageUrlFormat, id)) + .AsString(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return null; + } + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // extract mod info + string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); + string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; + if (name.StartsWith("[SMAPI] ")) + name = name.Substring("[SMAPI] ".Length); + string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; + + // create model + return new ChucklefishMod + { + Name = name, + Version = version, + Url = url + }; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs new file mode 100644 index 00000000..fd0101d4 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish +{ + /// Mod metadata from the Chucklefish mod site. + internal class ChucklefishMod + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs new file mode 100644 index 00000000..1d8b256e --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish +{ + /// An HTTP client for fetching mod metadata from the Chucklefish mod site. + internal interface IChucklefishClient : IDisposable + { + /********* + ** Methods + *********/ + /// Get metadata about a mod. + /// The Chucklefish mod ID. + /// Returns the mod info if found, else null. + Task GetModAsync(uint id); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs new file mode 100644 index 00000000..0b205660 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Web.Framework.Clients.GitHub +{ + /// An HTTP client for fetching metadata from GitHub. + internal class GitHubClient : IGitHubClient + { + /********* + ** Properties + *********/ + /// The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name. + private readonly string ReleaseUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the GitHub API. + /// The URL for a GitHub releases API query excluding the , where {0} is the repository owner and name. + /// The user agent for the API client. + /// The Accept header value expected by the GitHub API. + /// The username with which to authenticate to the GitHub API. + /// The password with which to authenticate to the GitHub API. + public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + { + this.ReleaseUrlFormat = releaseUrlFormat; + + this.Client = new FluentClient(baseUrl) + .SetUserAgent(userAgent) + .AddDefault(req => req.WithHeader("Accept", acceptHeader)); + if (!string.IsNullOrWhiteSpace(username)) + this.Client = this.Client.SetBasicAuthentication(username, password); + } + + /// Get the latest release for a GitHub repository. + /// The repository key (like Pathoschild/SMAPI). + /// Returns the latest release if found, else null. + public async Task GetLatestReleaseAsync(string repo) + { + // validate key format + if (!repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo)); + + // fetch info + try + { + return await this.Client + .GetAsync(string.Format(this.ReleaseUrlFormat, repo)) + .As(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return null; + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs new file mode 100644 index 00000000..0a47f3b4 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.GitHub +{ + /// A GitHub project release. + internal class GitRelease + { + /********* + ** Accessors + *********/ + /// The display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The semantic version string. + [JsonProperty("tag_name")] + public string Tag { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs new file mode 100644 index 00000000..6e8eadff --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.GitHub +{ + /// An HTTP client for fetching metadata from GitHub. + internal interface IGitHubClient : IDisposable + { + /********* + ** Methods + *********/ + /// Get the latest release for a GitHub repository. + /// The repository key (like Pathoschild/SMAPI). + /// Returns the latest release if found, else null. + Task GetLatestReleaseAsync(string repo); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs new file mode 100644 index 00000000..e56e7af4 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus +{ + /// An HTTP client for fetching mod metadata from Nexus Mods. + internal interface INexusClient : IDisposable + { + /********* + ** Methods + *********/ + /// Get metadata about a mod. + /// The Nexus mod ID. + /// Returns the mod info if found, else null. + Task GetModAsync(uint id); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs new file mode 100644 index 00000000..1a7011e2 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus +{ + /// An HTTP client for fetching mod metadata from Nexus Mods. + internal class NexusClient : INexusClient + { + /********* + ** Properties + *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ModUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the Nexus Mods API client. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public NexusClient(string userAgent, string baseUrl, string modUrlFormat) + { + this.ModUrlFormat = modUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod. + /// The Nexus mod ID. + /// Returns the mod info if found, else null. + public async Task GetModAsync(uint id) + { + return await this.Client + .GetAsync(string.Format(this.ModUrlFormat, id)) + .As(); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs new file mode 100644 index 00000000..2b04104f --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus +{ + /// Mod metadata from Nexus Mods. + internal class NexusMod + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs new file mode 100644 index 00000000..19794920 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -0,0 +1,56 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for the API clients. + internal class ApiClientsConfig + { + /********* + ** Accessors + *********/ + /**** + ** Generic + ****/ + /// The user agent for API clients, where {0} is the SMAPI version. + public string UserAgent { get; set; } + + + /**** + ** Chucklefish + ****/ + /// The base URL for the Chucklefish mod site. + public string ChucklefishBaseUrl { get; set; } + + /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. + public string ChucklefishModPageUrlFormat { get; set; } + + + /**** + ** GitHub + ****/ + /// The base URL for the GitHub API. + public string GitHubBaseUrl { get; set; } + + /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. + public string GitHubReleaseUrlFormat { get; set; } + + /// The Accept header value expected by the GitHub API. + public string GitHubAcceptHeader { get; set; } + + /// The username with which to authenticate to the GitHub API (if any). + public string GitHubUsername { get; set; } + + /// The password with which to authenticate to the GitHub API (if any). + public string GitHubPassword { get; set; } + + /**** + ** Nexus Mods + ****/ + /// The user agent for the Nexus Mods API client. + public string NexusUserAgent { get; set; } + + /// The base URL for the Nexus Mods API. + public string NexusBaseUrl { get; set; } + + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public string NexusModUrlFormat { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 2fb5b97e..58c3a100 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -6,9 +6,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /********* ** Accessors *********/ - /**** - ** General - ****/ /// The number of minutes update checks should be cached before refetching them. public int CacheMinutes { get; set; } @@ -16,59 +13,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// Derived from SMAPI's SemanticVersion implementation. public string SemanticVersionRegex { get; set; } - /**** - ** Chucklefish mod site - ****/ /// The repository key for the Chucklefish mod site. public string ChucklefishKey { get; set; } - /// The user agent for the Chucklefish API client, where {0} is the SMAPI version. - public string ChucklefishUserAgent { get; set; } - - /// The base URL for the Chucklefish mod site. - public string ChucklefishBaseUrl { get; set; } - - /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. - public string ChucklefishModPageUrlFormat { get; set; } - - - /**** - ** GitHub - ****/ /// The repository key for Nexus Mods. public string GitHubKey { get; set; } - /// The user agent for the GitHub API client, where {0} is the SMAPI version. - public string GitHubUserAgent { get; set; } - - /// The base URL for the GitHub API. - public string GitHubBaseUrl { get; set; } - - /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. - public string GitHubReleaseUrlFormat { get; set; } - - /// The Accept header value expected by the GitHub API. - public string GitHubAcceptHeader { get; set; } - - /// The username with which to authenticate to the GitHub API (if any). - public string GitHubUsername { get; set; } - - /// The password with which to authenticate to the GitHub API (if any). - public string GitHubPassword { get; set; } - - /**** - ** Nexus Mods - ****/ /// The repository key for Nexus Mods. public string NexusKey { get; set; } - - /// The user agent for the Nexus Mods API client. - public string NexusUserAgent { get; set; } - - /// The base URL for the Nexus Mods API. - public string NexusBaseUrl { get; set; } - - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - public string NexusModUrlFormat { get; set; } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 06ec58ed..266055a6 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -1,9 +1,7 @@ using System; -using System.Net; using System.Threading.Tasks; -using HtmlAgilityPack; -using Pathoschild.Http.Client; using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.Clients.Chucklefish; namespace StardewModdingAPI.Web.Framework.ModRepositories { @@ -13,14 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /********* ** Properties *********/ - /// The base URL for the Chucklefish mod site. - private readonly string BaseUrl; - - /// The URL for a mod page excluding the base URL, where {0} is the mod ID. - private readonly string ModPageUrlFormat; - /// The underlying HTTP client. - private readonly IClient Client; + private readonly IChucklefishClient Client; /********* @@ -28,15 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories *********/ /// Construct an instance. /// The unique key for this vendor. - /// The user agent for the API client. - /// The base URL for the Chucklefish mod site. - /// The URL for a mod page excluding the , where {0} is the mod ID. - public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) + /// The underlying HTTP client. + public ChucklefishRepository(string vendorKey, IChucklefishClient client) : base(vendorKey) { - this.BaseUrl = baseUrl; - this.ModPageUrlFormat = modPageUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.Client = client; } /// Get metadata about a mod in the repository. @@ -44,38 +32,18 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories public override async Task GetModInfoAsync(string id) { // validate ID format - if (!uint.TryParse(id, out uint _)) + if (!uint.TryParse(id, out uint realID)) return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); // fetch info try { - // fetch HTML - string html; - try - { - html = await this.Client - .GetAsync(string.Format(this.ModPageUrlFormat, id)) - .AsString(); - } - catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) - { + var mod = await this.Client.GetModAsync(realID); + if (mod == null) return new ModInfoModel("Found no mod with this ID."); - } - - // parse HTML - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - // extract mod info - string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); - string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; - if (name.StartsWith("[SMAPI] ")) - name = name.Substring("[SMAPI] ".Length); - string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; // create model - return new ModInfoModel(name, this.NormaliseVersion(version), url); + return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 9d43adf0..7bad6127 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,9 +1,7 @@ using System; -using System.Net; using System.Threading.Tasks; -using Newtonsoft.Json; -using Pathoschild.Http.Client; using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.Clients.GitHub; namespace StardewModdingAPI.Web.Framework.ModRepositories { @@ -13,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /********* ** Properties *********/ - /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. - private readonly string ReleaseUrlFormat; - - /// The underlying HTTP client. - private readonly IClient Client; + /// The underlying GitHub API client. + private readonly IGitHubClient Client; /********* @@ -25,22 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories *********/ /// Construct an instance. /// The unique key for this vendor. - /// The base URL for the Nexus Mods API. - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - /// The user agent for the API client. - /// The Accept header value expected by the GitHub API. - /// The username with which to authenticate to the GitHub API. - /// The password with which to authenticate to the GitHub API. - public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + /// The underlying GitHub API client. + public GitHubRepository(string vendorKey, IGitHubClient client) : base(vendorKey) { - this.ReleaseUrlFormat = releaseUrlFormat; - - this.Client = new FluentClient(baseUrl) - .SetUserAgent(userAgent) - .AddDefault(req => req.WithHeader("Accept", acceptHeader)); - if (!string.IsNullOrWhiteSpace(username)) - this.Client = this.Client.SetBasicAuthentication(username, password); + this.Client = client; } /// Get metadata about a mod in the repository. @@ -54,14 +38,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories // fetch info try { - GitRelease release = await this.Client - .GetAsync(string.Format(this.ReleaseUrlFormat, id)) - .As(); - return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); - } - catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) - { - return new ModInfoModel("Found no mod with this ID."); + GitRelease release = await this.Client.GetLatestReleaseAsync(id); + return release != null + ? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases") + : new ModInfoModel("Found no mod with this ID."); } catch (Exception ex) { @@ -74,24 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { this.Client.Dispose(); } - - - /********* - ** Private models - *********/ - /// Metadata about a GitHub release tag. - private class GitRelease - { - /********* - ** Accessors - *********/ - /// The display name. - [JsonProperty("name")] - public string Name { get; set; } - - /// The semantic version string. - [JsonProperty("tag_name")] - public string Tag { get; set; } - } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 8a4bb0d8..cfa757ab 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -1,8 +1,7 @@ using System; using System.Threading.Tasks; -using Newtonsoft.Json; -using Pathoschild.Http.Client; using StardewModdingAPI.Common.Models; +using StardewModdingAPI.Web.Framework.Clients.Nexus; namespace StardewModdingAPI.Web.Framework.ModRepositories { @@ -12,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /********* ** Properties *********/ - /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. - private readonly string ModUrlFormat; - - /// The underlying HTTP client. - private readonly IClient Client; + /// The underlying Nexus Mods API client. + private readonly INexusClient Client; /********* @@ -24,14 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories *********/ /// Construct an instance. /// The unique key for this vendor. - /// The user agent for the Nexus Mods API client. - /// The base URL for the Nexus Mods API. - /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) + /// The underlying Nexus Mods API client. + public NexusRepository(string vendorKey, INexusClient client) : base(vendorKey) { - this.ModUrlFormat = modUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.Client = client; } /// Get metadata about a mod in the repository. @@ -39,18 +32,15 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories public override async Task GetModInfoAsync(string id) { // validate ID format - if (!uint.TryParse(id, out uint _)) + if (!uint.TryParse(id, out uint nexusID)) return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); // fetch info try { - NexusResponseModel response = await this.Client - .GetAsync(string.Format(this.ModUrlFormat, id)) - .As(); - - return response != null - ? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url) + NexusMod mod = await this.Client.GetModAsync(nexusID); + return mod != null + ? new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url) : new ModInfoModel("Found no mod with this ID."); } catch (Exception ex) @@ -64,26 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { this.Client.Dispose(); } - - - /********* - ** Private models - *********/ - /// A mod metadata response from Nexus Mods. - private class NexusResponseModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's semantic version number. - public string Version { get; set; } - - /// The mod's web URL. - [JsonProperty("mod_page_uri")] - public string Url { get; set; } - } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 2f2b0d11..7938520a 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Clients.Chucklefish; +using StardewModdingAPI.Web.Framework.Clients.GitHub; +using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RewriteRules; @@ -41,6 +44,7 @@ namespace StardewModdingAPI.Web /// The service injection container. public void ConfigureServices(IServiceCollection services) { + // init configuration services .Configure(this.Configuration.GetSection("ModUpdateCheck")) .Configure(this.Configuration.GetSection("LogParser")) @@ -53,6 +57,32 @@ namespace StardewModdingAPI.Web options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); + + // init API clients + { + ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get(); + string version = this.GetType().Assembly.GetName().Version.ToString(3); + string userAgent = string.Format(api.UserAgent, version); + + services.AddSingleton(new ChucklefishClient( + userAgent: userAgent, + baseUrl: api.ChucklefishBaseUrl, + modPageUrlFormat: api.ChucklefishModPageUrlFormat + )); + services.AddSingleton(new GitHubClient( + baseUrl: api.GitHubBaseUrl, + releaseUrlFormat: api.GitHubReleaseUrlFormat, + userAgent: userAgent, + acceptHeader: api.GitHubAcceptHeader, + username: api.GitHubUsername, + password: api.GitHubPassword + )); + services.AddSingleton(new NexusClient( + userAgent: api.NexusUserAgent, + baseUrl: api.NexusBaseUrl, + modUrlFormat: api.NexusModUrlFormat + )); + } } /// The method called by the runtime to configure the HTTP request pipeline. diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 87c35ca9..4602d1ed 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -16,7 +16,7 @@ "Microsoft": "Information" } }, - "ModUpdateCheck": { + "ApiClients": { "GitHubUsername": null, "GitHubPassword": null }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index eb6ecc9b..4f70b41b 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -13,28 +13,31 @@ "Default": "Warning" } }, - "ModUpdateCheck": { - "CacheMinutes": 60, - "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", + "ApiClients": { + "UserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", - "ChucklefishKey": "Chucklefish", - "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", - "GitHubKey": "GitHub", - "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", "GitHubBaseUrl": "https://api.github.com", "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", "GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubUsername": null, // see top note "GitHubPassword": null, // see top note - "NexusKey": "Nexus", "NexusUserAgent": "Nexus Client v0.63.15", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", "NexusModUrlFormat": "mods/{0}" }, + + "ModUpdateCheck": { + "CacheMinutes": 60, + "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", + + "ChucklefishKey": "Chucklefish", + "GitHubKey": "GitHub", + "NexusKey": "Nexus" + }, "LogParser": { "SectionUrl": null, // see top note "PastebinBaseUrl": "https://pastebin.com/", From bbd021f8736d1496f34a58b12bb0ee6c341d1c5e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Dec 2017 23:40:23 -0500 Subject: [PATCH 25/34] decouple Pastebin client from log parser (#411) --- .../Controllers/LogParserController.cs | 20 ++++++------- .../Clients/Pastebin/IPastebinClient.cs | 17 +++++++++++ .../Pastebin/PasteInfo.cs} | 4 +-- .../Pastebin}/PastebinClient.cs | 30 +++++++++---------- .../Pastebin/SavePasteResult.cs} | 4 +-- .../ConfigModels/ApiClientsConfig.cs | 16 ++++++++++ .../Framework/ConfigModels/LogParserConfig.cs | 12 -------- src/SMAPI.Web/Startup.cs | 10 +++++++ src/SMAPI.Web/appsettings.Development.json | 9 +++--- src/SMAPI.Web/appsettings.json | 12 ++++---- 10 files changed, 82 insertions(+), 52 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs rename src/SMAPI.Web/Framework/{LogParser/GetPasteResponse.cs => Clients/Pastebin/PasteInfo.cs} (83%) rename src/SMAPI.Web/Framework/{LogParser => Clients/Pastebin}/PastebinClient.cs (77%) rename src/SMAPI.Web/Framework/{LogParser/SavePasteResponse.cs => Clients/Pastebin/SavePasteResult.cs} (82%) diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 454440bb..b9227a2f 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.LogParser; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Web.Controllers private readonly LogParserConfig Config; /// The underlying Pastebin client. - private readonly PastebinClient PastebinClient; + private readonly IPastebinClient Pastebin; /// The first bytes in a valid zip file. /// See . @@ -37,13 +37,11 @@ namespace StardewModdingAPI.Web.Controllers ***/ /// Construct an instance. /// The log parser config settings. - public LogParserController(IOptions configProvider) + /// The Pastebin API client. + public LogParserController(IOptions configProvider, IPastebinClient pastebin) { - // init Pastebin client this.Config = configProvider.Value; - string version = this.GetType().Assembly.GetName().Version.ToString(3); - string userAgent = string.Format(this.Config.PastebinUserAgent, version); - this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinUserKey, this.Config.PastebinDevKey); + this.Pastebin = pastebin; } /*** @@ -67,9 +65,9 @@ namespace StardewModdingAPI.Web.Controllers /// The Pastebin paste ID. [HttpGet, Produces("application/json")] [Route("log/fetch/{id}")] - public async Task GetAsync(string id) + public async Task GetAsync(string id) { - GetPasteResponse response = await this.PastebinClient.GetAsync(id); + PasteInfo response = await this.Pastebin.GetAsync(id); response.Content = this.DecompressString(response.Content); return response; } @@ -78,10 +76,10 @@ namespace StardewModdingAPI.Web.Controllers /// The log content to save. [HttpPost, Produces("application/json"), AllowLargePosts] [Route("log/save")] - public async Task PostAsync([FromBody] string content) + public async Task PostAsync([FromBody] string content) { content = this.CompressString(content); - return await this.PastebinClient.PostAsync(content); + return await this.Pastebin.PostAsync(content); } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs new file mode 100644 index 00000000..630dfb76 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin +{ + /// An API client for Pastebin. + internal interface IPastebinClient : IDisposable + { + /// Fetch a saved paste. + /// The paste ID. + Task GetAsync(string id); + + /// Save a paste to Pastebin. + /// The paste content. + Task PostAsync(string content); + } +} diff --git a/src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs similarity index 83% rename from src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs rename to src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs index 4f8794db..955156eb 100644 --- a/src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.Web.Framework.LogParser +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { /// The response for a get-paste request. - internal class GetPasteResponse + internal class PasteInfo { /// Whether the log was successfully fetched. public bool Success { get; set; } diff --git a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs similarity index 77% rename from src/SMAPI.Web/Framework/LogParser/PastebinClient.cs rename to src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs index 1cfaed17..ef83a91e 100644 --- a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -8,10 +8,10 @@ using System.Threading.Tasks; using System.Web; using Pathoschild.Http.Client; -namespace StardewModdingAPI.Web.Framework.LogParser +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { /// An API client for Pastebin. - internal class PastebinClient : IDisposable + internal class PastebinClient : IPastebinClient { /********* ** Properties @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.LogParser /// Fetch a saved paste. /// The paste ID. - public async Task GetAsync(string id) + public async Task GetAsync(string id) { try { @@ -54,30 +54,30 @@ namespace StardewModdingAPI.Web.Framework.LogParser // handle Pastebin errors if (string.IsNullOrWhiteSpace(content)) - return new GetPasteResponse { Error = "Received an empty response from Pastebin." }; + return new PasteInfo { Error = "Received an empty response from Pastebin." }; if (content.StartsWith("Save a paste to Pastebin. /// The paste content. - public async Task PostAsync(string content) + public async Task PostAsync(string content) { try { // validate if (string.IsNullOrWhiteSpace(content)) - return new SavePasteResponse { Error = "The log content can't be empty." }; + return new SavePasteResult { Error = "The log content can't be empty." }; // post to API string response = await this.Client @@ -96,19 +96,19 @@ namespace StardewModdingAPI.Web.Framework.LogParser // handle Pastebin errors if (string.IsNullOrWhiteSpace(response)) - return new SavePasteResponse { Error = "Received an empty response from Pastebin." }; + return new SavePasteResult { Error = "Received an empty response from Pastebin." }; if (response.StartsWith("Bad API request")) - return new SavePasteResponse { Error = response }; + return new SavePasteResult { Error = response }; if (!response.Contains("/")) - return new SavePasteResponse { Error = $"Received an unknown response: {response}" }; + return new SavePasteResult { Error = $"Received an unknown response: {response}" }; // return paste ID string pastebinID = response.Split("/").Last(); - return new SavePasteResponse { Success = true, ID = pastebinID }; + return new SavePasteResult { Success = true, ID = pastebinID }; } catch (Exception ex) { - return new SavePasteResponse { Success = false, Error = ex.ToString() }; + return new SavePasteResult { Success = false, Error = ex.ToString() }; } } diff --git a/src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/SavePasteResult.cs similarity index 82% rename from src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs rename to src/SMAPI.Web/Framework/Clients/Pastebin/SavePasteResult.cs index 1c0960a4..89dab697 100644 --- a/src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/SavePasteResult.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.Web.Framework.LogParser +namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { /// The response for a save-log request. - internal class SavePasteResponse + internal class SavePasteResult { /// Whether the log was successfully saved. public bool Success { get; set; } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index 19794920..61219414 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -52,5 +52,21 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. public string NexusModUrlFormat { get; set; } + + /**** + ** Pastebin + ****/ + /// The base URL for the Pastebin API. + public string PastebinBaseUrl { get; set; } + + /// The user agent for the Pastebin API client, where {0} is the SMAPI version. + public string PastebinUserAgent { get; set; } + + /// The user key used to authenticate with the Pastebin API. + public string PastebinUserKey { get; set; } + + /// The developer key used to authenticate with the Pastebin API. + public string PastebinDevKey { get; set; } + } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs index df5d605d..198274b2 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs @@ -8,17 +8,5 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels *********/ /// The root URL for the log parser controller. public string SectionUrl { get; set; } - - /// The base URL for the Pastebin API. - public string PastebinBaseUrl { get; set; } - - /// The user agent for the Pastebin API client, where {0} is the SMAPI version. - public string PastebinUserAgent { get; set; } - - /// The user key used to authenticate with the Pastebin API. - public string PastebinUserKey { get; set; } - - /// The developer key used to authenticate with the Pastebin API. - public string PastebinDevKey { get; set; } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 7938520a..307c4ae9 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -10,6 +10,7 @@ using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; +using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RewriteRules; @@ -69,6 +70,7 @@ namespace StardewModdingAPI.Web baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); + services.AddSingleton(new GitHubClient( baseUrl: api.GitHubBaseUrl, releaseUrlFormat: api.GitHubReleaseUrlFormat, @@ -77,11 +79,19 @@ namespace StardewModdingAPI.Web username: api.GitHubUsername, password: api.GitHubPassword )); + services.AddSingleton(new NexusClient( userAgent: api.NexusUserAgent, baseUrl: api.NexusBaseUrl, modUrlFormat: api.NexusModUrlFormat )); + + services.AddSingleton(new PastebinClient( + baseUrl: api.PastebinBaseUrl, + userAgent: userAgent, + userKey: api.PastebinUserKey, + devKey: api.PastebinDevKey + )); } } diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 4602d1ed..45fc30f3 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -18,11 +18,12 @@ }, "ApiClients": { "GitHubUsername": null, - "GitHubPassword": null - }, - "LogParser": { - "SectionUrl": "http://localhost:59482/log/", + "GitHubPassword": null, + "PastebinUserKey": null, "PastebinDevKey": null + }, + "LogParser": { + "SectionUrl": "http://localhost:59482/log/" } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 4f70b41b..69b3b4f8 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -27,7 +27,11 @@ "NexusUserAgent": "Nexus Client v0.63.15", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", - "NexusModUrlFormat": "mods/{0}" + "NexusModUrlFormat": "mods/{0}", + + "PastebinBaseUrl": "https://pastebin.com/", + "PastebinUserKey": null, // see top note + "PastebinDevKey": null // see top note }, "ModUpdateCheck": { @@ -39,10 +43,6 @@ "NexusKey": "Nexus" }, "LogParser": { - "SectionUrl": null, // see top note - "PastebinBaseUrl": "https://pastebin.com/", - "PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", - "PastebinUserKey": null, // see top note - "PastebinDevKey": null // see top note + "SectionUrl": null // see top note } } From adee66b3b4ea111b0082a31108e55726fab10643 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Dec 2017 01:47:10 -0500 Subject: [PATCH 26/34] add basic download page (#411) --- src/SMAPI.Web/Controllers/IndexController.cs | 79 +++++++++++++++++++ .../Controllers/LogParserController.cs | 1 - .../Framework/Clients/GitHub/GitAsset.cs | 20 +++++ .../Framework/Clients/GitHub/GitRelease.cs | 6 ++ src/SMAPI.Web/Properties/launchSettings.json | 2 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 1 + src/SMAPI.Web/Startup.cs | 1 - src/SMAPI.Web/ViewModels/IndexModel.cs | 41 ++++++++++ src/SMAPI.Web/Views/Index/Index.cshtml | 73 +++++++++++++++++ 9 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI.Web/Controllers/IndexController.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs create mode 100644 src/SMAPI.Web/ViewModels/IndexModel.cs create mode 100644 src/SMAPI.Web/Views/Index/Index.cshtml diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs new file mode 100644 index 00000000..c2c5f2fe --- /dev/null +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -0,0 +1,79 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using StardewModdingAPI.Web.Framework.Clients.GitHub; +using StardewModdingAPI.Web.ViewModels; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides an info/download page about SMAPI. + [Route("")] + [Route("install")] + internal class IndexController : Controller + { + /********* + ** Properties + *********/ + /// The GitHub API client. + private readonly IGitHubClient GitHub; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The GitHub API client. + public IndexController(IGitHubClient github) + { + this.GitHub = github; + } + + /// Display the index page. + [HttpGet] + public async Task Index() + { + // fetch latest SMAPI release + GitRelease release = await this.GitHub.GetLatestReleaseAsync("Pathoschild/SMAPI"); + string downloadUrl = this.GetMainDownloadUrl(release); + string devDownloadUrl = this.GetDevDownloadUrl(release); + + // render view + var model = new IndexModel(release.Name, release.Body, downloadUrl, devDownloadUrl); + return this.View(model); + } + + + /********* + ** Private methods + *********/ + /// Get the main download URL for a SMAPI release. + /// The SMAPI release. + private string GetMainDownloadUrl(GitRelease release) + { + // get main download URL + foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + { + if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer.zip")) + return asset.DownloadUrl; + } + + // fallback just in case + return "https://github.com/pathoschild/SMAPI/releases"; + } + + /// Get the for-developers download URL for a SMAPI release. + /// The SMAPI release. + private string GetDevDownloadUrl(GitRelease release) + { + // get dev download URL + foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + { + if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer-for-developers.zip")) + return asset.DownloadUrl; + } + + // fallback just in case + return "https://github.com/pathoschild/SMAPI/releases"; + } + } +} diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index b9227a2f..ad979397 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -50,7 +50,6 @@ namespace StardewModdingAPI.Web.Controllers /// Render the log parser UI. /// The paste ID. [HttpGet] - [Route("")] [Route("log")] [Route("log/{id}")] public ViewResult Index(string id = null) diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs new file mode 100644 index 00000000..73ce4025 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.GitHub +{ + /// A GitHub download attached to a release. + internal class GitAsset + { + /// The file name. + [JsonProperty("name")] + public string FileName { get; set; } + + /// The file content type. + [JsonProperty("content_type")] + public string ContentType { get; set; } + + /// The download URL. + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs index 0a47f3b4..b944088d 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -15,5 +15,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The semantic version string. [JsonProperty("tag_name")] public string Tag { get; set; } + + /// The Markdown description for the release. + public string Body { get; set; } + + /// The attached files. + public GitAsset[] Assets { get; set; } } } diff --git a/src/SMAPI.Web/Properties/launchSettings.json b/src/SMAPI.Web/Properties/launchSettings.json index e485e4e3..88179044 100644 --- a/src/SMAPI.Web/Properties/launchSettings.json +++ b/src/SMAPI.Web/Properties/launchSettings.json @@ -11,7 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "log", + "launchUrl": "", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index b5b0ff07..19198503 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -11,6 +11,7 @@ + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 307c4ae9..c6e3f85d 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -134,7 +134,6 @@ namespace StardewModdingAPI.Web // shortcut redirects .Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index")) - .Add(new RedirectToUrlRule("^/install$", "https://stardewvalleywiki.com/Modding:Installing_SMAPI")) ) .UseStaticFiles() // wwwroot folder .UseMvc(); diff --git a/src/SMAPI.Web/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs new file mode 100644 index 00000000..6d3da91e --- /dev/null +++ b/src/SMAPI.Web/ViewModels/IndexModel.cs @@ -0,0 +1,41 @@ +namespace StardewModdingAPI.Web.ViewModels +{ + /// The view model for the index page. + public class IndexModel + { + /********* + ** Accessors + *********/ + /// The latest SMAPI version. + public string LatestVersion { get; set; } + + /// The Markdown description for the release. + public string Description { get; set; } + + /// The main download URL. + public string DownloadUrl { get; set; } + + /// The for-developers download URL. + public string DevDownloadUrl { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public IndexModel() { } + + /// Construct an instance. + /// The latest SMAPI version. + /// The Markdown description for the release. + /// The main download URL. + /// The for-developers download URL. + internal IndexModel(string latestVersion, string description, string downloadUrl, string devDownloadUrl) + { + this.LatestVersion = latestVersion; + this.Description = description; + this.DownloadUrl = downloadUrl; + this.DevDownloadUrl = devDownloadUrl; + } + } +} diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml new file mode 100644 index 00000000..b2b8c0dd --- /dev/null +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -0,0 +1,73 @@ +@{ + ViewData["Title"] = "SMAPI"; +} +@model StardewModdingAPI.Web.ViewModels.IndexModel + + + +

+ SMAPI is the modding API for Stardew Valley. It works fine with Steam achievements and the + overlay, you can uninstall it anytime, and there's a friendly community if you need help. It's + a cool boy. +

+ +

Download and links

+
+ +

What's new in SMAPI @Model.LatestVersion?

+
+ @Html.Raw(Markdig.Markdown.ToHtml(Model.Description)) +
+ +

Support SMAPI ♥

+ + +

+ Special thanks to + acerbicon, + ChefRude, + jwdred, + Karmylla, + OfficialPiAddict, + Robby LaFarge, + and a few anonymous users for supporting SMAPI; you're awesome! +

+ +

For mod creators

+ From 70cbfa541dade1822394f81bd1d5b6baa18e51e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Dec 2017 01:53:10 -0500 Subject: [PATCH 27/34] support contextual nav URLs, update nav menu (#411) --- src/SMAPI.Web/Controllers/LogParserController.cs | 10 +++++----- .../Framework/ConfigModels/ContextConfig.cs | 15 +++++++++++++++ .../Framework/ConfigModels/LogParserConfig.cs | 12 ------------ src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 10 +++++++--- src/SMAPI.Web/appsettings.Development.json | 7 ++++--- src/SMAPI.Web/appsettings.json | 7 ++++--- 7 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs delete mode 100644 src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index ad979397..04a11a82 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Web.Controllers ** Properties *********/ /// The log parser config settings. - private readonly LogParserConfig Config; + private readonly ContextConfig Config; /// The underlying Pastebin client. private readonly IPastebinClient Pastebin; @@ -36,11 +36,11 @@ namespace StardewModdingAPI.Web.Controllers ** Constructor ***/ /// Construct an instance. - /// The log parser config settings. + /// The context config settings. /// The Pastebin API client. - public LogParserController(IOptions configProvider, IPastebinClient pastebin) + public LogParserController(IOptions contextProvider, IPastebinClient pastebin) { - this.Config = configProvider.Value; + this.Config = contextProvider.Value; this.Pastebin = pastebin; } @@ -54,7 +54,7 @@ namespace StardewModdingAPI.Web.Controllers [Route("log/{id}")] public ViewResult Index(string id = null) { - return this.View("Index", new LogParserModel(this.Config.SectionUrl, id)); + return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id)); } /*** diff --git a/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs new file mode 100644 index 00000000..117462f4 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for the app context. + public class ContextConfig // must be public to pass into views + { + /********* + ** Accessors + *********/ + /// The root URL for the app. + public string RootUrl { get; set; } + + /// The root URL for the log parser. + public string LogParserUrl { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs deleted file mode 100644 index 198274b2..00000000 --- a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ConfigModels -{ - /// The config settings for the log parser. - internal class LogParserConfig - { - /********* - ** Accessors - *********/ - /// The root URL for the log parser controller. - public string SectionUrl { get; set; } - } -} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index c6e3f85d..e5e759e7 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Web // init configuration services .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(this.Configuration.GetSection("LogParser")) + .Configure(this.Configuration.GetSection("Context")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 547a8178..ac98c71b 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -1,3 +1,7 @@ +@using Microsoft.Extensions.Options +@using StardewModdingAPI.Web.Framework.ConfigModels +@inject IOptions ContextConfig + @@ -10,9 +14,9 @@
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 45fc30f3..495af120 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -16,14 +16,15 @@ "Microsoft": "Information" } }, + "Context": { + "RootUrl": "http://localhost:59482/", + "LogParserUrl": "http://localhost:59482/log/" + }, "ApiClients": { "GitHubUsername": null, "GitHubPassword": null, "PastebinUserKey": null, "PastebinDevKey": null - }, - "LogParser": { - "SectionUrl": "http://localhost:59482/log/" } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 69b3b4f8..9758f4a7 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -13,6 +13,10 @@ "Default": "Warning" } }, + "Context": { + "RootUrl": null, // see top note + "LogParserUrl": null // see top note + }, "ApiClients": { "UserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", @@ -41,8 +45,5 @@ "ChucklefishKey": "Chucklefish", "GitHubKey": "GitHub", "NexusKey": "Nexus" - }, - "LogParser": { - "SectionUrl": null // see top note } } From 3da98ff0a032255d8b567d27554f13abe1dba62e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Dec 2017 02:18:24 -0500 Subject: [PATCH 28/34] cache release info (#411) --- src/SMAPI.Web/Controllers/IndexController.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index c2c5f2fe..5d45118f 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -1,6 +1,8 @@ +using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; @@ -14,17 +16,25 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Properties *********/ + /// The cache in which to store release data. + private readonly IMemoryCache Cache; + /// The GitHub API client. private readonly IGitHubClient GitHub; + /// The cache time for release info. + private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(5); + /********* ** Public methods *********/ /// Construct an instance. + /// The cache in which to store release data. /// The GitHub API client. - public IndexController(IGitHubClient github) + public IndexController(IMemoryCache cache, IGitHubClient github) { + this.Cache = cache; this.GitHub = github; } @@ -33,7 +43,11 @@ namespace StardewModdingAPI.Web.Controllers public async Task Index() { // fetch latest SMAPI release - GitRelease release = await this.GitHub.GetLatestReleaseAsync("Pathoschild/SMAPI"); + GitRelease release = await this.Cache.GetOrCreateAsync("latest-smapi-release", async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); + return await this.GitHub.GetLatestReleaseAsync("Pathoschild/SMAPI"); + }); string downloadUrl = this.GetMainDownloadUrl(release); string devDownloadUrl = this.GetDevDownloadUrl(release); From 05136e69f10a40998181423a6e507386d620ff98 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Dec 2017 10:26:31 -0500 Subject: [PATCH 29/34] prettify download page (#411) --- src/SMAPI.Web/Views/Index/Index.cshtml | 48 +++++++---------- src/SMAPI.Web/wwwroot/Content/css/index.css | 58 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/Content/css/index.css diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index b2b8c0dd..93dd389f 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -2,38 +2,26 @@ ViewData["Title"] = "SMAPI"; } @model StardewModdingAPI.Web.ViewModels.IndexModel +@section Head { + +} - - -

- SMAPI is the modding API for Stardew Valley. It works fine with Steam achievements and the - overlay, you can uninstall it anytime, and there's a friendly community if you need help. It's - a cool boy. +

+ The mod loader for Stardew Valley. It works fine with GOG and Steam achievements, compatible + with Linux/Mac/Windows, you can uninstall it anytime, and there's a friendly community if + you need help. It's a cool pufferchick.

-

Download and links

+ + + +

Find help

@@ -43,8 +31,10 @@ @Html.Raw(Markdig.Markdown.ToHtml(Model.Description))
+

See the release notes for more info.

+

Support SMAPI ♥

-
    +