diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-24 17:54:31 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-02-24 17:54:31 -0500 |
commit | 414cf5c197b5b59776d3dda914eb15710efb0868 (patch) | |
tree | 0393a95194ad78cf4440c68657b0488b7db6d68b /src/SMAPI | |
parent | 5da8b707385b9851ff3f6442de58519125f5c96f (diff) | |
parent | f2e8450706d1971d774f870081deffdb0c6b92eb (diff) | |
download | SMAPI-414cf5c197b5b59776d3dda914eb15710efb0868.tar.gz SMAPI-414cf5c197b5b59776d3dda914eb15710efb0868.tar.bz2 SMAPI-414cf5c197b5b59776d3dda914eb15710efb0868.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
63 files changed, 3823 insertions, 2886 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 515e9870..8fe2b55b 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -21,6 +21,14 @@ namespace StardewModdingAPI /// <summary>Whether the directory containing the current save's data exists on disk.</summary> private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); + /// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID). This doesn't affect update checks, which defer to the remote web API.</summary> + private static readonly IDictionary<string, string> VendorModUrls = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["GitHub"] = "https://github.com/{0}/releases", + ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" + }; + /********* ** Accessors @@ -29,7 +37,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.4.0"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.5.0"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); @@ -87,14 +95,6 @@ namespace StardewModdingAPI Platform.Mono; #endif - /// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API.</summary> - internal static readonly IDictionary<string, string> VendorModUrls = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) - { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", - ["GitHub"] = "https://github.com/{0}/releases" - }; - /********* ** Internal methods @@ -145,6 +145,23 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } + /// <summary>Get an update URL for an update key (if valid).</summary> + /// <param name="updateKey">The update key.</param> + internal static string GetUpdateUrl(string updateKey) + { + string[] parts = updateKey.Split(new[] { ':' }, 2); + if (parts.Length != 2) + return null; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (Constants.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + return string.Format(urlTemplate, modID); + + return null; + } + /********* ** Private methods diff --git a/src/SMAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs index 4b4e2ad0..63645258 100644 --- a/src/SMAPI/Events/ContentEvents.cs +++ b/src/SMAPI/Events/ContentEvents.cs @@ -1,29 +1,37 @@ -using System; -using StardewModdingAPI.Framework; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { /// <summary>Events raised when the game loads content.</summary> public static class ContentEvents { + /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + /********* ** Events *********/ /// <summary>Raised after the content language changes.</summary> - public static event EventHandler<EventArgsValueChanged<string>> AfterLocaleChanged; + public static event EventHandler<EventArgsValueChanged<string>> AfterLocaleChanged + { + add => ContentEvents.EventManager.Content_LocaleChanged.Add(value); + remove => ContentEvents.EventManager.Content_LocaleChanged.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// <summary>Raise an <see cref="AfterLocaleChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="oldLocale">The previous locale.</param> - /// <param name="newLocale">The current locale.</param> - internal static void InvokeAfterLocaleChanged(IMonitor monitor, string oldLocale, string newLocale) + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterLocaleChanged)}", ContentEvents.AfterLocaleChanged?.GetInvocationList(), null, new EventArgsValueChanged<string>(oldLocale, newLocale)); + ContentEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs index 80d0f547..973bb245 100644 --- a/src/SMAPI/Events/ControlEvents.cs +++ b/src/SMAPI/Events/ControlEvents.cs @@ -1,7 +1,6 @@ -using System; -using Microsoft.Xna.Framework; +using System; using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -9,104 +8,80 @@ namespace StardewModdingAPI.Events public static class ControlEvents { /********* - ** Events + ** Properties *********/ - /// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary> - public static event EventHandler<EventArgsKeyboardStateChanged> KeyboardChanged; - - /// <summary>Raised when the player presses a keyboard key.</summary> - public static event EventHandler<EventArgsKeyPressed> KeyPressed; - - /// <summary>Raised when the player releases a keyboard key.</summary> - public static event EventHandler<EventArgsKeyPressed> KeyReleased; - - /// <summary>Raised when the <see cref="MouseState"/> changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.</summary> - public static event EventHandler<EventArgsMouseStateChanged> MouseChanged; - - /// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary> - public static event EventHandler<EventArgsControllerButtonPressed> ControllerButtonPressed; - - /// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary> - public static event EventHandler<EventArgsControllerButtonReleased> ControllerButtonReleased; - - /// <summary>The player pressed a controller trigger button.</summary> - public static event EventHandler<EventArgsControllerTriggerPressed> ControllerTriggerPressed; - - /// <summary>The player released a controller trigger button.</summary> - public static event EventHandler<EventArgsControllerTriggerReleased> ControllerTriggerReleased; + /// <summary>The core event manager.</summary> + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// <summary>Raise a <see cref="KeyboardChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorState">The previous keyboard state.</param> - /// <param name="newState">The current keyboard state.</param> - internal static void InvokeKeyboardChanged(IMonitor monitor, KeyboardState priorState, KeyboardState newState) + /// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary> + public static event EventHandler<EventArgsKeyboardStateChanged> KeyboardChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyboardChanged)}", ControlEvents.KeyboardChanged?.GetInvocationList(), null, new EventArgsKeyboardStateChanged(priorState, newState)); + add => ControlEvents.EventManager.Control_KeyboardChanged.Add(value); + remove => ControlEvents.EventManager.Control_KeyboardChanged.Remove(value); } - /// <summary>Raise a <see cref="MouseChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorState">The previous mouse state.</param> - /// <param name="newState">The current mouse state.</param> - /// <param name="priorPosition">The previous mouse position on the screen adjusted for the zoom level.</param> - /// <param name="newPosition">The current mouse position on the screen adjusted for the zoom level.</param> - internal static void InvokeMouseChanged(IMonitor monitor, MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) + /// <summary>Raised when the player presses a keyboard key.</summary> + public static event EventHandler<EventArgsKeyPressed> KeyPressed { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.MouseChanged)}", ControlEvents.MouseChanged?.GetInvocationList(), null, new EventArgsMouseStateChanged(priorState, newState, priorPosition, newPosition)); + add => ControlEvents.EventManager.Control_KeyPressed.Add(value); + remove => ControlEvents.EventManager.Control_KeyPressed.Remove(value); } - /// <summary>Raise a <see cref="KeyPressed"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="key">The keyboard button that was pressed.</param> - internal static void InvokeKeyPressed(IMonitor monitor, Keys key) + /// <summary>Raised when the player releases a keyboard key.</summary> + public static event EventHandler<EventArgsKeyPressed> KeyReleased { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyPressed)}", ControlEvents.KeyPressed?.GetInvocationList(), null, new EventArgsKeyPressed(key)); + add => ControlEvents.EventManager.Control_KeyReleased.Add(value); + remove => ControlEvents.EventManager.Control_KeyReleased.Remove(value); } - /// <summary>Raise a <see cref="KeyReleased"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="key">The keyboard button that was released.</param> - internal static void InvokeKeyReleased(IMonitor monitor, Keys key) + /// <summary>Raised when the <see cref="MouseState"/> changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.</summary> + public static event EventHandler<EventArgsMouseStateChanged> MouseChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.KeyReleased)}", ControlEvents.KeyReleased?.GetInvocationList(), null, new EventArgsKeyPressed(key)); + add => ControlEvents.EventManager.Control_MouseChanged.Add(value); + remove => ControlEvents.EventManager.Control_MouseChanged.Remove(value); } - /// <summary>Raise a <see cref="ControllerButtonPressed"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="button">The controller button that was pressed.</param> - internal static void InvokeButtonPressed(IMonitor monitor, Buttons button) + /// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary> + public static event EventHandler<EventArgsControllerButtonPressed> ControllerButtonPressed { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(PlayerIndex.One, button)); + add => ControlEvents.EventManager.Control_ControllerButtonPressed.Add(value); + remove => ControlEvents.EventManager.Control_ControllerButtonPressed.Remove(value); } - /// <summary>Raise a <see cref="ControllerButtonReleased"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="button">The controller button that was released.</param> - internal static void InvokeButtonReleased(IMonitor monitor, Buttons button) + /// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary> + public static event EventHandler<EventArgsControllerButtonReleased> ControllerButtonReleased { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(PlayerIndex.One, button)); + add => ControlEvents.EventManager.Control_ControllerButtonReleased.Add(value); + remove => ControlEvents.EventManager.Control_ControllerButtonReleased.Remove(value); } - /// <summary>Raise a <see cref="ControllerTriggerPressed"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="button">The trigger button that was pressed.</param> - /// <param name="value">The current trigger value.</param> - internal static void InvokeTriggerPressed(IMonitor monitor, Buttons button, float value) + /// <summary>The player pressed a controller trigger button.</summary> + public static event EventHandler<EventArgsControllerTriggerPressed> ControllerTriggerPressed { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(PlayerIndex.One, button, value)); + add => ControlEvents.EventManager.Control_ControllerTriggerPressed.Add(value); + remove => ControlEvents.EventManager.Control_ControllerTriggerPressed.Remove(value); } - /// <summary>Raise a <see cref="ControllerTriggerReleased"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="button">The trigger button that was pressed.</param> - /// <param name="value">The current trigger value.</param> - internal static void InvokeTriggerReleased(IMonitor monitor, Buttons button, float value) + /// <summary>The player released a controller trigger button.</summary> + public static event EventHandler<EventArgsControllerTriggerReleased> ControllerTriggerReleased + { + add => ControlEvents.EventManager.Control_ControllerTriggerReleased.Add(value); + remove => ControlEvents.EventManager.Control_ControllerTriggerReleased.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(PlayerIndex.One, button, value)); + ControlEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs index 3466470d..92879280 100644 --- a/src/SMAPI/Events/GameEvents.cs +++ b/src/SMAPI/Events/GameEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,100 +7,80 @@ namespace StardewModdingAPI.Events public static class GameEvents { /********* - ** Events + ** Properties *********/ - /// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary> - internal static event EventHandler InitializeInternal; - - /// <summary>Raised when the game updates its state (≈60 times per second).</summary> - public static event EventHandler UpdateTick; - - /// <summary>Raised every other tick (≈30 times per second).</summary> - public static event EventHandler SecondUpdateTick; - - /// <summary>Raised every fourth tick (≈15 times per second).</summary> - public static event EventHandler FourthUpdateTick; - - /// <summary>Raised every eighth tick (≈8 times per second).</summary> - public static event EventHandler EighthUpdateTick; - - /// <summary>Raised every 15th tick (≈4 times per second).</summary> - public static event EventHandler QuarterSecondTick; - - /// <summary>Raised every 30th tick (≈twice per second).</summary> - public static event EventHandler HalfSecondTick; - - /// <summary>Raised every 60th tick (≈once per second).</summary> - public static event EventHandler OneSecondTick; - - /// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary> - public static event EventHandler FirstUpdateTick; + /// <summary>The core event manager.</summary> + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// <summary>Raise an <see cref="InitializeInternal"/> event.</summary> - /// <param name="monitor">Encapsulates logging and monitoring.</param> - internal static void InvokeInitialize(IMonitor monitor) + /// <summary>Raised when the game updates its state (≈60 times per second).</summary> + public static event EventHandler UpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); + add => GameEvents.EventManager.Game_UpdateTick.Add(value); + remove => GameEvents.EventManager.Game_UpdateTick.Remove(value); } - /// <summary>Raise an <see cref="UpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates logging and monitoring.</param> - internal static void InvokeUpdateTick(IMonitor monitor) + /// <summary>Raised every other tick (≈30 times per second).</summary> + public static event EventHandler SecondUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_SecondUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_SecondUpdateTick.Remove(value); } - /// <summary>Raise a <see cref="SecondUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeSecondUpdateTick(IMonitor monitor) + /// <summary>Raised every fourth tick (≈15 times per second).</summary> + public static event EventHandler FourthUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_FourthUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_FourthUpdateTick.Remove(value); } - /// <summary>Raise a <see cref="FourthUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeFourthUpdateTick(IMonitor monitor) + /// <summary>Raised every eighth tick (≈8 times per second).</summary> + public static event EventHandler EighthUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_EighthUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_EighthUpdateTick.Remove(value); } - /// <summary>Raise a <see cref="EighthUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeEighthUpdateTick(IMonitor monitor) + /// <summary>Raised every 15th tick (≈4 times per second).</summary> + public static event EventHandler QuarterSecondTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_QuarterSecondTick.Add(value); + remove => GameEvents.EventManager.Game_QuarterSecondTick.Remove(value); } - /// <summary>Raise a <see cref="QuarterSecondTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeQuarterSecondTick(IMonitor monitor) + /// <summary>Raised every 30th tick (≈twice per second).</summary> + public static event EventHandler HalfSecondTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_HalfSecondTick.Add(value); + remove => GameEvents.EventManager.Game_HalfSecondTick.Remove(value); } - /// <summary>Raise a <see cref="HalfSecondTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeHalfSecondTick(IMonitor monitor) + /// <summary>Raised every 60th tick (≈once per second).</summary> + public static event EventHandler OneSecondTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_OneSecondTick.Add(value); + remove => GameEvents.EventManager.Game_OneSecondTick.Remove(value); } - /// <summary>Raise a <see cref="OneSecondTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOneSecondTick(IMonitor monitor) + /// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary> + public static event EventHandler FirstUpdateTick { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); + add => GameEvents.EventManager.Game_FirstUpdateTick.Add(value); + remove => GameEvents.EventManager.Game_FirstUpdateTick.Remove(value); } - /// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeFirstUpdateTick(IMonitor monitor) + + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList()); + GameEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs index fff51bed..e1ff4ee7 100644 --- a/src/SMAPI/Events/GraphicsEvents.cs +++ b/src/SMAPI/Events/GraphicsEvents.cs @@ -1,5 +1,5 @@ -using System; -using StardewModdingAPI.Framework; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,110 +7,82 @@ namespace StardewModdingAPI.Events public static class GraphicsEvents { /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + + + /********* ** Events *********/ - /**** - ** Generic events - ****/ /// <summary>Raised after the game window is resized.</summary> - public static event EventHandler Resize; + public static event EventHandler Resize + { + add => GraphicsEvents.EventManager.Graphics_Resize.Add(value); + remove => GraphicsEvents.EventManager.Graphics_Resize.Remove(value); + } /**** ** Main render events ****/ /// <summary>Raised before drawing the world to the screen.</summary> - public static event EventHandler OnPreRenderEvent; + public static event EventHandler OnPreRenderEvent + { + add => GraphicsEvents.EventManager.Graphics_OnPreRenderEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPreRenderEvent.Remove(value); + } /// <summary>Raised after drawing the world to the screen.</summary> - public static event EventHandler OnPostRenderEvent; - - /**** - ** HUD events - ****/ - /// <summary>Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary> - public static event EventHandler OnPreRenderHudEvent; - - /// <summary>Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary> - public static event EventHandler OnPostRenderHudEvent; - - /**** - ** GUI events - ****/ - /// <summary>Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary> - public static event EventHandler OnPreRenderGuiEvent; - - /// <summary>Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary> - public static event EventHandler OnPostRenderGuiEvent; - - - /********* - ** Internal methods - *********/ - /**** - ** Generic events - ****/ - /// <summary>Raise a <see cref="Resize"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeResize(IMonitor monitor) + public static event EventHandler OnPostRenderEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPostRenderEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPostRenderEvent.Remove(value); } /**** - ** Main render events + ** HUD events ****/ - /// <summary>Raise an <see cref="OnPreRenderEvent"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOnPreRenderEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderEvent)}", GraphicsEvents.OnPreRenderEvent?.GetInvocationList()); - } - - /// <summary>Raise an <see cref="OnPostRenderEvent"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOnPostRenderEvent(IMonitor monitor) + /// <summary>Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary> + public static event EventHandler OnPreRenderHudEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderEvent)}", GraphicsEvents.OnPostRenderEvent?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPreRenderHudEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPreRenderHudEvent.Remove(value); } - /// <summary>Get whether there are any post-render event listeners.</summary> - internal static bool HasPostRenderListeners() + /// <summary>Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary> + public static event EventHandler OnPostRenderHudEvent { - return GraphicsEvents.OnPostRenderEvent != null; + add => GraphicsEvents.EventManager.Graphics_OnPostRenderHudEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPostRenderHudEvent.Remove(value); } /**** ** GUI events ****/ - /// <summary>Raise an <see cref="OnPreRenderGuiEvent"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOnPreRenderGuiEvent(IMonitor monitor) + /// <summary>Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary> + public static event EventHandler OnPreRenderGuiEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderGuiEvent)}", GraphicsEvents.OnPreRenderGuiEvent?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPreRenderGuiEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPreRenderGuiEvent.Remove(value); } - /// <summary>Raise an <see cref="OnPostRenderGuiEvent"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOnPostRenderGuiEvent(IMonitor monitor) + /// <summary>Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary> + public static event EventHandler OnPostRenderGuiEvent { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderGuiEvent)}", GraphicsEvents.OnPostRenderGuiEvent?.GetInvocationList()); + add => GraphicsEvents.EventManager.Graphics_OnPostRenderGuiEvent.Add(value); + remove => GraphicsEvents.EventManager.Graphics_OnPostRenderGuiEvent.Remove(value); } - /**** - ** HUD events - ****/ - /// <summary>Raise an <see cref="OnPreRenderHudEvent"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOnPreRenderHudEvent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPreRenderHudEvent)}", GraphicsEvents.OnPreRenderHudEvent?.GetInvocationList()); - } - /// <summary>Raise an <see cref="OnPostRenderHudEvent"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeOnPostRenderHudEvent(IMonitor monitor) + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.OnPostRenderHudEvent)}", GraphicsEvents.OnPostRenderHudEvent?.GetInvocationList()); + GraphicsEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs index 985aed99..84d7ce5d 100644 --- a/src/SMAPI/Events/InputEvents.cs +++ b/src/SMAPI/Events/InputEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,38 +7,38 @@ namespace StardewModdingAPI.Events public static class InputEvents { /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + + + /********* ** Events *********/ /// <summary>Raised when the player presses a button on the keyboard, controller, or mouse.</summary> - public static event EventHandler<EventArgsInput> ButtonPressed; + public static event EventHandler<EventArgsInput> ButtonPressed + { + add => InputEvents.EventManager.Input_ButtonPressed.Add(value); + remove => InputEvents.EventManager.Input_ButtonPressed.Remove(value); + } /// <summary>Raised when the player releases a keyboard key on the keyboard, controller, or mouse.</summary> - public static event EventHandler<EventArgsInput> ButtonReleased; + public static event EventHandler<EventArgsInput> ButtonReleased + { + add => InputEvents.EventManager.Input_ButtonReleased.Add(value); + remove => InputEvents.EventManager.Input_ButtonReleased.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// <summary>Raise a <see cref="ButtonPressed"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="button">The button on the controller, keyboard, or mouse.</param> - /// <param name="cursor">The cursor position.</param> - /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param> - /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param> - internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) - { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); - } - - /// <summary>Raise a <see cref="ButtonReleased"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="button">The button on the controller, keyboard, or mouse.</param> - /// <param name="cursor">The cursor position.</param> - /// <param name="isActionButton">Whether the input should trigger actions on the affected tile.</param> - /// <param name="isUseToolButton">Whether the input should use tools on the affected tile.</param> - internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isActionButton, bool isUseToolButton) + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isActionButton, isUseToolButton)); + InputEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs index b834bc1c..81d13e9f 100644 --- a/src/SMAPI/Events/LocationEvents.cs +++ b/src/SMAPI/Events/LocationEvents.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework; -using StardewModdingAPI.Framework; -using StardewValley; -using Object = StardewValley.Object; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -11,44 +7,45 @@ namespace StardewModdingAPI.Events public static class LocationEvents { /********* - ** Events + ** Properties *********/ - /// <summary>Raised after the player warps to a new location.</summary> - public static event EventHandler<EventArgsCurrentLocationChanged> CurrentLocationChanged; - - /// <summary>Raised after a game location is added or removed.</summary> - public static event EventHandler<EventArgsGameLocationsChanged> LocationsChanged; - - /// <summary>Raised after the list of objects in the current location changes (e.g. an object is added or removed).</summary> - public static event EventHandler<EventArgsLocationObjectsChanged> LocationObjectsChanged; + /// <summary>The core event manager.</summary> + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// <summary>Raise a <see cref="CurrentLocationChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorLocation">The player's previous location.</param> - /// <param name="newLocation">The player's current location.</param> - internal static void InvokeCurrentLocationChanged(IMonitor monitor, GameLocation priorLocation, GameLocation newLocation) + /// <summary>Raised after the player warps to a new location.</summary> + public static event EventHandler<EventArgsCurrentLocationChanged> CurrentLocationChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.CurrentLocationChanged)}", LocationEvents.CurrentLocationChanged?.GetInvocationList(), null, new EventArgsCurrentLocationChanged(priorLocation, newLocation)); + add => LocationEvents.EventManager.Location_CurrentLocationChanged.Add(value); + remove => LocationEvents.EventManager.Location_CurrentLocationChanged.Remove(value); } - /// <summary>Raise a <see cref="LocationsChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="newLocations">The current list of game locations.</param> - internal static void InvokeLocationsChanged(IMonitor monitor, List<GameLocation> newLocations) + /// <summary>Raised after a game location is added or removed.</summary> + public static event EventHandler<EventArgsGameLocationsChanged> LocationsChanged + { + add => LocationEvents.EventManager.Location_LocationsChanged.Add(value); + remove => LocationEvents.EventManager.Location_LocationsChanged.Remove(value); + } + + /// <summary>Raised after the list of objects in the current location changes (e.g. an object is added or removed).</summary> + public static event EventHandler<EventArgsLocationObjectsChanged> LocationObjectsChanged { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationsChanged)}", LocationEvents.LocationsChanged?.GetInvocationList(), null, new EventArgsGameLocationsChanged(newLocations)); + add => LocationEvents.EventManager.Location_LocationObjectsChanged.Add(value); + remove => LocationEvents.EventManager.Location_LocationObjectsChanged.Remove(value); } - /// <summary>Raise a <see cref="LocationObjectsChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="newObjects">The current list of objects in the current location.</param> - internal static void InvokeOnNewLocationObject(IMonitor monitor, SerializableDictionary<Vector2, Object> newObjects) + + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(LocationEvents)}.{nameof(LocationEvents.LocationObjectsChanged)}", LocationEvents.LocationObjectsChanged?.GetInvocationList(), null, new EventArgsLocationObjectsChanged(newObjects)); + LocationEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs index bd8d897e..7fcc3844 100644 --- a/src/SMAPI/Events/MenuEvents.cs +++ b/src/SMAPI/Events/MenuEvents.cs @@ -1,6 +1,5 @@ -using System; -using StardewModdingAPI.Framework; -using StardewValley.Menus; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -8,33 +7,38 @@ namespace StardewModdingAPI.Events public static class MenuEvents { /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + + + /********* ** Events *********/ /// <summary>Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed.</summary> - public static event EventHandler<EventArgsClickableMenuChanged> MenuChanged; + public static event EventHandler<EventArgsClickableMenuChanged> MenuChanged + { + add => MenuEvents.EventManager.Menu_Changed.Add(value); + remove => MenuEvents.EventManager.Menu_Changed.Remove(value); + } /// <summary>Raised after a game menu is closed.</summary> - public static event EventHandler<EventArgsClickableMenuClosed> MenuClosed; + public static event EventHandler<EventArgsClickableMenuClosed> MenuClosed + { + add => MenuEvents.EventManager.Menu_Closed.Add(value); + remove => MenuEvents.EventManager.Menu_Closed.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// <summary>Raise a <see cref="MenuChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorMenu">The previous menu.</param> - /// <param name="newMenu">The current menu.</param> - internal static void InvokeMenuChanged(IMonitor monitor, IClickableMenu priorMenu, IClickableMenu newMenu) - { - monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuChanged)}", MenuEvents.MenuChanged?.GetInvocationList(), null, new EventArgsClickableMenuChanged(priorMenu, newMenu)); - } - - /// <summary>Raise a <see cref="MenuClosed"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorMenu">The menu that was closed.</param> - internal static void InvokeMenuClosed(IMonitor monitor, IClickableMenu priorMenu) + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(MenuEvents)}.{nameof(MenuEvents.MenuClosed)}", MenuEvents.MenuClosed?.GetInvocationList(), null, new EventArgsClickableMenuClosed(priorMenu)); + MenuEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs index 9cf7edac..5ee4001b 100644 --- a/src/SMAPI/Events/MineEvents.cs +++ b/src/SMAPI/Events/MineEvents.cs @@ -1,5 +1,5 @@ -using System; -using StardewModdingAPI.Framework; +using System; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,22 +7,31 @@ namespace StardewModdingAPI.Events public static class MineEvents { /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + + + /********* ** Events *********/ /// <summary>Raised after the player warps to a new level of the mine.</summary> - public static event EventHandler<EventArgsMineLevelChanged> MineLevelChanged; + public static event EventHandler<EventArgsMineLevelChanged> MineLevelChanged + { + add => MineEvents.EventManager.Mine_LevelChanged.Add(value); + remove => MineEvents.EventManager.Mine_LevelChanged.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// <summary>Raise a <see cref="MineLevelChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="previousMineLevel">The previous mine level.</param> - /// <param name="currentMineLevel">The current mine level.</param> - internal static void InvokeMineLevelChanged(IMonitor monitor, int previousMineLevel, int currentMineLevel) + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(MineEvents)}.{nameof(MineEvents.MineLevelChanged)}", MineEvents.MineLevelChanged?.GetInvocationList(), null, new EventArgsMineLevelChanged(previousMineLevel, currentMineLevel)); + MineEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs index 5a9a9d5f..84a7ff63 100644 --- a/src/SMAPI/Events/PlayerEvents.cs +++ b/src/SMAPI/Events/PlayerEvents.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using StardewModdingAPI.Framework; -using StardewValley; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -10,34 +7,38 @@ namespace StardewModdingAPI.Events public static class PlayerEvents { /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + + + /********* ** Events *********/ /// <summary>Raised after the player's inventory changes in any way (added or removed item, sorted, etc).</summary> - public static event EventHandler<EventArgsInventoryChanged> InventoryChanged; + public static event EventHandler<EventArgsInventoryChanged> InventoryChanged + { + add => PlayerEvents.EventManager.Player_InventoryChanged.Add(value); + remove => PlayerEvents.EventManager.Player_InventoryChanged.Remove(value); + } /// <summary> Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed.</summary> - public static event EventHandler<EventArgsLevelUp> LeveledUp; + public static event EventHandler<EventArgsLevelUp> LeveledUp + { + add => PlayerEvents.EventManager.Player_LeveledUp.Add(value); + remove => PlayerEvents.EventManager.Player_LeveledUp.Remove(value); + } /********* - ** Internal methods + ** Public methods *********/ - /// <summary>Raise an <see cref="InventoryChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="inventory">The player's inventory.</param> - /// <param name="changedItems">The inventory changes.</param> - internal static void InvokeInventoryChanged(IMonitor monitor, List<Item> inventory, IEnumerable<ItemStackChange> changedItems) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList())); - } - - /// <summary>Rase a <see cref="LeveledUp"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="type">The player skill that leveled up.</param> - /// <param name="newLevel">The new skill level.</param> - internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel) + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel)); + PlayerEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs index 99b6c8d2..62184282 100644 --- a/src/SMAPI/Events/SaveEvents.cs +++ b/src/SMAPI/Events/SaveEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,70 +7,66 @@ namespace StardewModdingAPI.Events public static class SaveEvents { /********* - ** Events + ** Properties *********/ - /// <summary>Raised before the game creates the save file.</summary> - public static event EventHandler BeforeCreate; - - /// <summary>Raised after the game finishes creating the save file.</summary> - public static event EventHandler AfterCreate; - - /// <summary>Raised before the game begins writes data to the save file.</summary> - public static event EventHandler BeforeSave; - - /// <summary>Raised after the game finishes writing data to the save file.</summary> - public static event EventHandler AfterSave; - - /// <summary>Raised after the player loads a save slot.</summary> - public static event EventHandler AfterLoad; - - /// <summary>Raised after the game returns to the title screen.</summary> - public static event EventHandler AfterReturnToTitle; + /// <summary>The core event manager.</summary> + private static EventManager EventManager; /********* - ** Internal methods + ** Events *********/ - /// <summary>Raise a <see cref="BeforeCreate"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeBeforeCreate(IMonitor monitor) + /// <summary>Raised before the game creates the save file.</summary> + public static event EventHandler BeforeCreate + { + add => SaveEvents.EventManager.Save_BeforeCreate.Add(value); + remove => SaveEvents.EventManager.Save_BeforeCreate.Remove(value); + } + + /// <summary>Raised after the game finishes creating the save file.</summary> + public static event EventHandler AfterCreate { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeCreate)}", SaveEvents.BeforeCreate?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterCreate.Add(value); + remove => SaveEvents.EventManager.Save_AfterCreate.Remove(value); } - /// <summary>Raise a <see cref="AfterCreate"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeAfterCreated(IMonitor monitor) + /// <summary>Raised before the game begins writes data to the save file.</summary> + public static event EventHandler BeforeSave { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterCreate)}", SaveEvents.AfterCreate?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_BeforeSave.Add(value); + remove => SaveEvents.EventManager.Save_BeforeSave.Remove(value); } - /// <summary>Raise a <see cref="BeforeSave"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeBeforeSave(IMonitor monitor) + /// <summary>Raised after the game finishes writing data to the save file.</summary> + public static event EventHandler AfterSave { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.BeforeSave)}", SaveEvents.BeforeSave?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterSave.Add(value); + remove => SaveEvents.EventManager.Save_AfterSave.Remove(value); } - /// <summary>Raise a <see cref="AfterSave"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeAfterSave(IMonitor monitor) + /// <summary>Raised after the player loads a save slot.</summary> + public static event EventHandler AfterLoad { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterSave)}", SaveEvents.AfterSave?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterLoad.Add(value); + remove => SaveEvents.EventManager.Save_AfterLoad.Remove(value); } - /// <summary>Raise a <see cref="AfterLoad"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeAfterLoad(IMonitor monitor) + /// <summary>Raised after the game returns to the title screen.</summary> + public static event EventHandler AfterReturnToTitle { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterLoad)}", SaveEvents.AfterLoad?.GetInvocationList(), null, EventArgs.Empty); + add => SaveEvents.EventManager.Save_AfterReturnToTitle.Add(value); + remove => SaveEvents.EventManager.Save_AfterReturnToTitle.Remove(value); } - /// <summary>Raise a <see cref="AfterReturnToTitle"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeAfterReturnToTitle(IMonitor monitor) + + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaisePlainEvent($"{nameof(SaveEvents)}.{nameof(SaveEvents.AfterReturnToTitle)}", SaveEvents.AfterReturnToTitle?.GetInvocationList(), null, EventArgs.Empty); + SaveEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs new file mode 100644 index 00000000..33ebf3b2 --- /dev/null +++ b/src/SMAPI/Events/SpecialisedEvents.cs @@ -0,0 +1,37 @@ +using System; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// <summary>Events serving specialised edge cases that shouldn't be used by most mod.</summary> + public static class SpecialisedEvents + { + /********* + ** Properties + *********/ + /// <summary>The core event manager.</summary> + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// <summary>Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console.</summary> + public static event EventHandler UnvalidatedUpdateTick + { + add => SpecialisedEvents.EventManager.Specialised_UnvalidatedUpdateTick.Add(value); + remove => SpecialisedEvents.EventManager.Specialised_UnvalidatedUpdateTick.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) + { + SpecialisedEvents.EventManager = eventManager; + } + } +} diff --git a/src/SMAPI/Events/TimeEvents.cs b/src/SMAPI/Events/TimeEvents.cs index 9aea5e04..f769fd08 100644 --- a/src/SMAPI/Events/TimeEvents.cs +++ b/src/SMAPI/Events/TimeEvents.cs @@ -1,5 +1,5 @@ using System; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Events { @@ -7,31 +7,38 @@ namespace StardewModdingAPI.Events public static class TimeEvents { /********* - ** Events + ** Properties *********/ - /// <summary>Raised after the game begins a new day, including when loading a save.</summary> - public static event EventHandler AfterDayStarted; + /// <summary>The core event manager.</summary> + private static EventManager EventManager; - /// <summary>Raised after the in-game clock changes.</summary> - public static event EventHandler<EventArgsIntChanged> TimeOfDayChanged; /********* - ** Internal methods + ** Events *********/ - /// <summary>Raise an <see cref="AfterDayStarted"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - internal static void InvokeAfterDayStarted(IMonitor monitor) + /// <summary>Raised after the game begins a new day, including when loading a save.</summary> + public static event EventHandler AfterDayStarted + { + add => TimeEvents.EventManager.Time_AfterDayStarted.Add(value); + remove => TimeEvents.EventManager.Time_AfterDayStarted.Remove(value); + } + + /// <summary>Raised after the in-game clock changes.</summary> + public static event EventHandler<EventArgsIntChanged> TimeOfDayChanged { - monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); + add => TimeEvents.EventManager.Time_TimeOfDayChanged.Add(value); + remove => TimeEvents.EventManager.Time_TimeOfDayChanged.Remove(value); } - /// <summary>Raise a <see cref="TimeOfDayChanged"/> event.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="priorTime">The previous time in military time format (e.g. 6:00pm is 1800).</param> - /// <param name="newTime">The current time in military time format (e.g. 6:10pm is 1810).</param> - internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime) + + /********* + ** Public methods + *********/ + /// <summary>Initialise the events.</summary> + /// <param name="eventManager">The core event manager.</param> + internal static void Init(EventManager eventManager) { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); + TimeEvents.EventManager = eventManager; } } } diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 4508e641..533da398 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewValley; namespace StardewModdingAPI.Framework.Content @@ -18,12 +19,6 @@ namespace StardewModdingAPI.Framework.Content /// <summary>The underlying asset cache.</summary> private readonly IDictionary<string, object> Cache; - /// <summary>The possible directory separator characters in an asset key.</summary> - private readonly char[] PossiblePathSeparators; - - /// <summary>The preferred directory separator chaeacter in an asset key.</summary> - private readonly string PreferredPathSeparator; - /// <summary>Applies platform-specific asset key normalisation so it's consistent with the underlying cache.</summary> private readonly Func<string, string> NormaliseAssetNameForPlatform; @@ -52,14 +47,10 @@ namespace StardewModdingAPI.Framework.Content /// <summary>Construct an instance.</summary> /// <param name="contentManager">The underlying content manager whose cache to manage.</param> /// <param name="reflection">Simplifies access to private game code.</param> - /// <param name="possiblePathSeparators">The possible directory separator characters in an asset key.</param> - /// <param name="preferredPathSeparator">The preferred directory separator chaeacter in an asset key.</param> - public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) + public ContentCache(LocalizedContentManager contentManager, Reflector reflection) { // init this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue(); - this.PossiblePathSeparators = possiblePathSeparators; - this.PreferredPathSeparator = preferredPathSeparator; // get key normalisation logic if (Constants.TargetPlatform == Platform.Windows) @@ -90,11 +81,7 @@ namespace StardewModdingAPI.Framework.Content [Pure] public string NormalisePathSeparators(string path) { - string[] parts = path.Split(this.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - string normalised = string.Join(this.PreferredPathSeparator, parts); - if (path.StartsWith(this.PreferredPathSeparator)) - normalised = this.PreferredPathSeparator + normalised; // keep root slash - return normalised; + return PathUtilities.NormalisePathSeparators(path); } /// <summary>Normalise a cache key so it's consistent with the underlying cache.</summary> diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs new file mode 100644 index 00000000..071fb872 --- /dev/null +++ b/src/SMAPI/Framework/ContentPack.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; +using xTile; + +namespace StardewModdingAPI.Framework +{ + /// <summary>Manages access to a content pack's metadata and files.</summary> + internal class ContentPack : IContentPack + { + /********* + ** Properties + *********/ + /// <summary>Provides an API for loading content assets.</summary> + private readonly IContentHelper Content; + + /// <summary>Encapsulates SMAPI's JSON file parsing.</summary> + private readonly JsonHelper JsonHelper; + + + /********* + ** Accessors + *********/ + /// <summary>The full path to the content pack's folder.</summary> + public string DirectoryPath { get; } + + /// <summary>The content pack's manifest.</summary> + public IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="directoryPath">The full path to the content pack's folder.</param> + /// <param name="manifest">The content pack's manifest.</param> + /// <param name="content">Provides an API for loading content assets.</param> + /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> + public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, JsonHelper jsonHelper) + { + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Content = content; + this.JsonHelper = jsonHelper; + } + + /// <summary>Read a JSON file from the content pack folder.</summary> + /// <typeparam name="TModel">The model type.</typeparam> + /// <param name="path">The file path relative to the contnet directory.</param> + /// <returns>Returns the deserialised model, or <c>null</c> if the file doesn't exist or is empty.</returns> + public TModel ReadJsonFile<TModel>(string path) where TModel : class + { + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + return this.JsonHelper.ReadJsonFile<TModel>(path); + } + + /// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> + /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam> + /// <param name="key">The local path to a content file relative to the content pack folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> + public T LoadAsset<T>(string key) + { + return this.Content.Load<T>(key, ContentSource.ModFolder); + } + + /// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary> + /// <param name="key">The the local path to a content file relative to the content pack folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + public string GetActualAssetKey(string key) + { + return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); + } + + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs new file mode 100644 index 00000000..d7c89a76 --- /dev/null +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -0,0 +1,249 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>Manages SMAPI events.</summary> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Private fields are deliberately named to simplify organisation.")] + internal class EventManager + { + /********* + ** Properties + *********/ + /**** + ** ContentEvents + ****/ + /// <summary>Raised after the content language changes.</summary> + public readonly ManagedEvent<EventArgsValueChanged<string>> Content_LocaleChanged; + + /**** + ** ControlEvents + ****/ + /// <summary>Raised when the <see cref="KeyboardState"/> changes. That happens when the player presses or releases a key.</summary> + public readonly ManagedEvent<EventArgsKeyboardStateChanged> Control_KeyboardChanged; + + /// <summary>Raised when the player presses a keyboard key.</summary> + public readonly ManagedEvent<EventArgsKeyPressed> Control_KeyPressed; + + /// <summary>Raised when the player releases a keyboard key.</summary> + public readonly ManagedEvent<EventArgsKeyPressed> Control_KeyReleased; + + /// <summary>Raised when the <see cref="MouseState"/> changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button.</summary> + public readonly ManagedEvent<EventArgsMouseStateChanged> Control_MouseChanged; + + /// <summary>The player pressed a controller button. This event isn't raised for trigger buttons.</summary> + public readonly ManagedEvent<EventArgsControllerButtonPressed> Control_ControllerButtonPressed; + + /// <summary>The player released a controller button. This event isn't raised for trigger buttons.</summary> + public readonly ManagedEvent<EventArgsControllerButtonReleased> Control_ControllerButtonReleased; + + /// <summary>The player pressed a controller trigger button.</summary> + public readonly ManagedEvent<EventArgsControllerTriggerPressed> Control_ControllerTriggerPressed; + + /// <summary>The player released a controller trigger button.</summary> + public readonly ManagedEvent<EventArgsControllerTriggerReleased> Control_ControllerTriggerReleased; + + /**** + ** GameEvents + ****/ + /// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary> + public readonly ManagedEvent Game_FirstUpdateTick; + + /// <summary>Raised when the game updates its state (≈60 times per second).</summary> + public readonly ManagedEvent Game_UpdateTick; + + /// <summary>Raised every other tick (≈30 times per second).</summary> + public readonly ManagedEvent Game_SecondUpdateTick; + + /// <summary>Raised every fourth tick (≈15 times per second).</summary> + public readonly ManagedEvent Game_FourthUpdateTick; + + /// <summary>Raised every eighth tick (≈8 times per second).</summary> + public readonly ManagedEvent Game_EighthUpdateTick; + + /// <summary>Raised every 15th tick (≈4 times per second).</summary> + public readonly ManagedEvent Game_QuarterSecondTick; + + /// <summary>Raised every 30th tick (≈twice per second).</summary> + public readonly ManagedEvent Game_HalfSecondTick; + + /// <summary>Raised every 60th tick (≈once per second).</summary> + public readonly ManagedEvent Game_OneSecondTick; + + /**** + ** GraphicsEvents + ****/ + /// <summary>Raised after the game window is resized.</summary> + public readonly ManagedEvent Graphics_Resize; + + /// <summary>Raised before drawing the world to the screen.</summary> + public readonly ManagedEvent Graphics_OnPreRenderEvent; + + /// <summary>Raised after drawing the world to the screen.</summary> + public readonly ManagedEvent Graphics_OnPostRenderEvent; + + /// <summary>Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary> + public readonly ManagedEvent Graphics_OnPreRenderHudEvent; + + /// <summary>Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.)</summary> + public readonly ManagedEvent Graphics_OnPostRenderHudEvent; + + /// <summary>Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary> + public readonly ManagedEvent Graphics_OnPreRenderGuiEvent; + + /// <summary>Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen.</summary> + public readonly ManagedEvent Graphics_OnPostRenderGuiEvent; + + /**** + ** InputEvents + ****/ + /// <summary>Raised when the player presses a button on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<EventArgsInput> Input_ButtonPressed; + + /// <summary>Raised when the player releases a keyboard key on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<EventArgsInput> Input_ButtonReleased; + + /**** + ** LocationEvents + ****/ + /// <summary>Raised after the player warps to a new location.</summary> + public readonly ManagedEvent<EventArgsCurrentLocationChanged> Location_CurrentLocationChanged; + + /// <summary>Raised after a game location is added or removed.</summary> + public readonly ManagedEvent<EventArgsGameLocationsChanged> Location_LocationsChanged; + + /// <summary>Raised after the list of objects in the current location changes (e.g. an object is added or removed).</summary> + public readonly ManagedEvent<EventArgsLocationObjectsChanged> Location_LocationObjectsChanged; + + /**** + ** MenuEvents + ****/ + /// <summary>Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed.</summary> + public readonly ManagedEvent<EventArgsClickableMenuChanged> Menu_Changed; + + /// <summary>Raised after a game menu is closed.</summary> + public readonly ManagedEvent<EventArgsClickableMenuClosed> Menu_Closed; + + /**** + ** MineEvents + ****/ + /// <summary>Raised after the player warps to a new level of the mine.</summary> + public readonly ManagedEvent<EventArgsMineLevelChanged> Mine_LevelChanged; + + /**** + ** PlayerEvents + ****/ + /// <summary>Raised after the player's inventory changes in any way (added or removed item, sorted, etc).</summary> + public readonly ManagedEvent<EventArgsInventoryChanged> Player_InventoryChanged; + + /// <summary> Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed.</summary> + public readonly ManagedEvent<EventArgsLevelUp> Player_LeveledUp; + + /**** + ** SaveEvents + ****/ + /// <summary>Raised before the game creates the save file.</summary> + public readonly ManagedEvent Save_BeforeCreate; + + /// <summary>Raised after the game finishes creating the save file.</summary> + public readonly ManagedEvent Save_AfterCreate; + + /// <summary>Raised before the game begins writes data to the save file.</summary> + public readonly ManagedEvent Save_BeforeSave; + + /// <summary>Raised after the game finishes writing data to the save file.</summary> + public readonly ManagedEvent Save_AfterSave; + + /// <summary>Raised after the player loads a save slot.</summary> + public readonly ManagedEvent Save_AfterLoad; + + /// <summary>Raised after the game returns to the title screen.</summary> + public readonly ManagedEvent Save_AfterReturnToTitle; + + /**** + ** SpecialisedEvents + ****/ + /// <summary>Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console.</summary> + public readonly ManagedEvent Specialised_UnvalidatedUpdateTick; + + /**** + ** TimeEvents + ****/ + /// <summary>Raised after the game begins a new day, including when loading a save.</summary> + public readonly ManagedEvent Time_AfterDayStarted; + + /// <summary>Raised after the in-game clock changes.</summary> + public readonly ManagedEvent<EventArgsIntChanged> Time_TimeOfDayChanged; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitor">Writes messages to the log.</param> + /// <param name="modRegistry">The mod registry with which to identify mods.</param> + public EventManager(IMonitor monitor, ModRegistry modRegistry) + { + // create shortcut initialisers + ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry); + ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + + // init events + this.Content_LocaleChanged = ManageEventOf<EventArgsValueChanged<string>>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); + + this.Control_ControllerButtonPressed = ManageEventOf<EventArgsControllerButtonPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Control_ControllerButtonReleased = ManageEventOf<EventArgsControllerButtonReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Control_ControllerTriggerPressed = ManageEventOf<EventArgsControllerTriggerPressed>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Control_ControllerTriggerReleased = ManageEventOf<EventArgsControllerTriggerReleased>(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Control_KeyboardChanged = ManageEventOf<EventArgsKeyboardStateChanged>(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Control_KeyPressed = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Control_KeyReleased = ManageEventOf<EventArgsKeyPressed>(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Control_MouseChanged = ManageEventOf<EventArgsMouseStateChanged>(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + + this.Game_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); + this.Game_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); + this.Game_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); + this.Game_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); + this.Game_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); + this.Game_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); + this.Game_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); + this.Game_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); + + this.Graphics_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); + this.Graphics_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); + this.Graphics_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); + this.Graphics_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); + this.Graphics_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); + this.Graphics_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); + this.Graphics_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); + + this.Input_ButtonPressed = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Input_ButtonReleased = ManageEventOf<EventArgsInput>(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + + this.Location_CurrentLocationChanged = ManageEventOf<EventArgsCurrentLocationChanged>(nameof(LocationEvents), nameof(LocationEvents.CurrentLocationChanged)); + this.Location_LocationsChanged = ManageEventOf<EventArgsGameLocationsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Location_LocationObjectsChanged = ManageEventOf<EventArgsLocationObjectsChanged>(nameof(LocationEvents), nameof(LocationEvents.LocationObjectsChanged)); + + this.Menu_Changed = ManageEventOf<EventArgsClickableMenuChanged>(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); + this.Menu_Closed = ManageEventOf<EventArgsClickableMenuClosed>(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + + this.Mine_LevelChanged = ManageEventOf<EventArgsMineLevelChanged>(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); + + this.Player_InventoryChanged = ManageEventOf<EventArgsInventoryChanged>(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); + this.Player_LeveledUp = ManageEventOf<EventArgsLevelUp>(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + + this.Save_BeforeCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); + this.Save_AfterCreate = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); + this.Save_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); + this.Save_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); + this.Save_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); + this.Save_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); + + this.Specialised_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); + + this.Time_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); + this.Time_TimeOfDayChanged = ManageEventOf<EventArgsIntChanged>(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs new file mode 100644 index 00000000..e54a4fd3 --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -0,0 +1,119 @@ +using System; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>An event wrapper which intercepts and logs errors in handler code.</summary> + /// <typeparam name="TEventArgs">The event arguments type.</typeparam> + internal class ManagedEvent<TEventArgs> : ManagedEventBase<EventHandler<TEventArgs>> + { + /********* + ** Properties + *********/ + /// <summary>The underlying event.</summary> + private event EventHandler<TEventArgs> Event; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="eventName">A human-readable name for the event.</param> + /// <param name="monitor">Writes messages to the log.</param> + /// <param name="modRegistry">The mod registry with which to identify mods.</param> + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// <summary>Add an event handler.</summary> + /// <param name="handler">The event handler.</param> + public void Add(EventHandler<TEventArgs> handler) + { + this.Event += handler; + this.AddTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>()); + } + + /// <summary>Remove an event handler.</summary> + /// <param name="handler">The event handler.</param> + public void Remove(EventHandler<TEventArgs> handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>()); + } + + /// <summary>Raise the event and notify all handlers.</summary> + /// <param name="args">The event arguments to pass.</param> + public void Raise(TEventArgs args) + { + if (this.Event == null) + return; + + foreach (EventHandler<TEventArgs> handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } + + /// <summary>An event wrapper which intercepts and logs errors in handler code.</summary> + internal class ManagedEvent : ManagedEventBase<EventHandler> + { + /********* + ** Properties + *********/ + /// <summary>The underlying event.</summary> + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="eventName">A human-readable name for the event.</param> + /// <param name="monitor">Writes messages to the log.</param> + /// <param name="modRegistry">The mod registry with which to identify mods.</param> + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// <summary>Add an event handler.</summary> + /// <param name="handler">The event handler.</param> + public void Add(EventHandler handler) + { + this.Event += handler; + this.AddTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler>()); + } + + /// <summary>Remove an event handler.</summary> + /// <param name="handler">The event handler.</param> + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler>()); + } + + /// <summary>Raise the event and notify all handlers.</summary> + public void Raise() + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, EventArgs.Empty); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs new file mode 100644 index 00000000..cc4d89ec --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEventBase.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// <summary>The base implementation for an event wrapper which intercepts and logs errors in handler code.</summary> + internal abstract class ManagedEventBase<TEventHandler> + { + /********* + ** Properties + *********/ + /// <summary>A human-readable name for the event.</summary> + private readonly string EventName; + + /// <summary>Writes messages to the log.</summary> + private readonly IMonitor Monitor; + + /// <summary>The mod registry with which to identify mods.</summary> + private readonly ModRegistry ModRegistry; + + /// <summary>The display names for the mods which added each delegate.</summary> + private readonly IDictionary<TEventHandler, IModMetadata> SourceMods = new Dictionary<TEventHandler, IModMetadata>(); + + /// <summary>The cached invocation list.</summary> + protected TEventHandler[] CachedInvocationList { get; private set; } + + + /********* + ** Public methods + *********/ + /// <summary>Get whether anything is listening to the event.</summary> + public bool HasListeners() + { + return this.CachedInvocationList?.Length > 0; + } + + /********* + ** Protected methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="eventName">A human-readable name for the event.</param> + /// <param name="monitor">Writes messages to the log.</param> + /// <param name="modRegistry">The mod registry with which to identify mods.</param> + protected ManagedEventBase(string eventName, IMonitor monitor, ModRegistry modRegistry) + { + this.EventName = eventName; + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// <summary>Track an event handler.</summary> + /// <param name="handler">The event handler.</param> + /// <param name="invocationList">The updated event invocation list.</param> + protected void AddTracking(TEventHandler handler, IEnumerable<TEventHandler> invocationList) + { + this.SourceMods[handler] = this.ModRegistry.GetFromStack(); + this.CachedInvocationList = invocationList.ToArray(); + } + + /// <summary>Remove tracking for an event handler.</summary> + /// <param name="handler">The event handler.</param> + /// <param name="invocationList">The updated event invocation list.</param> + protected void RemoveTracking(TEventHandler handler, IEnumerable<TEventHandler> invocationList) + { + this.SourceMods.Remove(handler); + this.CachedInvocationList = invocationList.ToArray(); + } + + /// <summary>Log an exception from an event handler.</summary> + /// <param name="handler">The event handler instance.</param> + /// <param name="ex">The exception that was raised.</param> + protected void LogError(TEventHandler handler, Exception ex) + { + if (this.SourceMods.TryGetValue(handler, out IModMetadata mod)) + mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + } + } +} diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index a36994fd..d1e8eb7d 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; namespace StardewModdingAPI.Framework @@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework /// <summary>The mod manifest.</summary> IManifest Manifest { get; } - /// <summary>>Metadata about the mod from SMAPI's internal data (if any).</summary> - ModDataRecord DataRecord { get; } + /// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary> + ParsedModDataRecord DataRecord { get; } /// <summary>The metadata resolution status.</summary> ModMetadataStatus Status { get; } @@ -27,12 +27,21 @@ namespace StardewModdingAPI.Framework /// <summary>The reason the metadata is invalid, if any.</summary> string Error { get; } - /// <summary>The mod instance (if it was loaded).</summary> + /// <summary>The mod instance (if loaded and <see cref="IsContentPack"/> is false).</summary> IMod Mod { get; } + /// <summary>The content pack instance (if loaded and <see cref="IsContentPack"/> is true).</summary> + IContentPack ContentPack { get; } + + /// <summary>Writes messages to the console and log file as this mod.</summary> + IMonitor Monitor { get; } + /// <summary>The mod-provided API (if any).</summary> object Api { get; } + /// <summary>Whether the mod is a content pack.</summary> + bool IsContentPack { get; } + /********* ** Public methods @@ -47,6 +56,11 @@ namespace StardewModdingAPI.Framework /// <param name="mod">The mod instance to set.</param> IModMetadata SetMod(IMod mod); + /// <summary>Set the mod instance.</summary> + /// <param name="contentPack">The contentPack instance to set.</param> + /// <param name="monitor">Writes messages to the console and log file.</param> + IModMetadata SetMod(IContentPack contentPack, IMonitor monitor); + /// <summary>Set the mod-provided API instance.</summary> /// <param name="api">The mod-provided API.</param> IModMetadata SetApi(object api); diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index bec6c183..bff4807c 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -14,63 +14,6 @@ namespace StardewModdingAPI.Framework /**** ** IMonitor ****/ - /// <summary>Safely raise an <see cref="EventHandler"/> event, and intercept any exceptions thrown by its handlers.</summary> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="name">The event name for error messages.</param> - /// <param name="handlers">The event handlers.</param> - /// <param name="sender">The event sender.</param> - /// <param name="args">The event arguments (or <c>null</c> to pass <see cref="EventArgs.Empty"/>).</param> - public static void SafelyRaisePlainEvent(this IMonitor monitor, string name, IEnumerable<Delegate> handlers, object sender = null, EventArgs args = null) - { - if (handlers == null) - return; - - foreach (EventHandler handler in handlers.Cast<EventHandler>()) - { - // handle SMAPI exiting - if (monitor.IsExiting) - { - monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn); - return; - } - - // raise event - try - { - handler.Invoke(sender, args ?? EventArgs.Empty); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - - /// <summary>Safely raise an <see cref="EventHandler{TEventArgs}"/> event, and intercept any exceptions thrown by its handlers.</summary> - /// <typeparam name="TEventArgs">The event argument object type.</typeparam> - /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="name">The event name for error messages.</param> - /// <param name="handlers">The event handlers.</param> - /// <param name="sender">The event sender.</param> - /// <param name="args">The event arguments.</param> - public static void SafelyRaiseGenericEvent<TEventArgs>(this IMonitor monitor, string name, IEnumerable<Delegate> handlers, object sender, TEventArgs args) - { - if (handlers == null) - return; - - foreach (EventHandler<TEventArgs> handler in handlers.Cast<EventHandler<TEventArgs>>()) - { - try - { - handler.Invoke(sender, args); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - /// <summary>Log a message for the player or developer the first time it occurs.</summary> /// <param name="monitor">The monitor through which to log the message.</param> /// <param name="hash">The hash of logged messages.</param> @@ -86,6 +29,18 @@ namespace StardewModdingAPI.Framework } /**** + ** IModMetadata + ****/ + /// <summary>Log a message using the mod's monitor.</summary> + /// <param name="metadata">The mod whose monitor to use.</param> + /// <param name="message">The message to log.</param> + /// <param name="level">The log severity level.</param> + public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) + { + metadata.Monitor.Log(message, level); + } + + /**** ** Exceptions ****/ /// <summary>Get a string representation of an exception suitable for writing to the error log.</summary> @@ -136,5 +91,20 @@ namespace StardewModdingAPI.Framework // get result return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue(); } + + /**** + ** Json.NET + ****/ + /// <summary>Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity.</summary> + /// <typeparam name="T">The value type.</typeparam> + /// <param name="obj">The JSON object to search.</param> + /// <param name="fieldName">The field name.</param> + public static T ValueIgnoreCase<T>(this JObject obj, string fieldName) + { + JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); + return token != null + ? token.Value<T>() + : default(T); + } } } diff --git a/src/SMAPI/Framework/ModData/ModDataField.cs b/src/SMAPI/Framework/ModData/ModDataField.cs new file mode 100644 index 00000000..fa8dd6d0 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Framework.ModData +{ + /// <summary>A versioned mod metadata field.</summary> + internal class ModDataField + { + /********* + ** Accessors + *********/ + /// <summary>The field key.</summary> + public ModDataFieldKey Key { get; } + + /// <summary>The field value.</summary> + public string Value { get; } + + /// <summary>Whether this field should only be applied if it's not already set.</summary> + public bool IsDefault { get; } + + /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> + public ISemanticVersion LowerVersion { get; } + + /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="key">The field key.</param> + /// <param name="value">The field value.</param> + /// <param name="isDefault">Whether this field should only be applied if it's not already set.</param> + /// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param> + /// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param> + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// <summary>Get whether this data field applies for the given manifest.</summary> + /// <param name="manifest">The mod manifest.</param> + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get whether a manifest field has a meaningful value for the purposes of enforcing <see cref="IsDefault"/>.</summary> + /// <param name="manifest">The mod manifest.</param> + /// <param name="key">The field key matching <see cref="ModDataFieldKey"/>.</param> + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI/Framework/ModData/ModDataFieldKey.cs b/src/SMAPI/Framework/ModData/ModDataFieldKey.cs new file mode 100644 index 00000000..f68f575c --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.ModData +{ + /// <summary>The valid field keys.</summary> + public enum ModDataFieldKey + { + /// <summary>A manifest update key.</summary> + UpdateKey, + + /// <summary>An alternative URL the player can check for an updated version.</summary> + AlternativeUrl, + + /// <summary>The mod's predefined compatibility status.</summary> + Status, + + /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary> + StatusReasonPhrase + } +} diff --git a/src/SMAPI/Framework/ModData/ModDataRecord.cs b/src/SMAPI/Framework/ModData/ModDataRecord.cs new file mode 100644 index 00000000..79a954f7 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDataRecord.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Framework.ModData +{ + /// <summary>Raw mod metadata from SMAPI's internal mod list.</summary> + internal class ModDataRecord + { + /********* + ** Properties + *********/ + /// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary> + [JsonExtensionData] + private IDictionary<string, JToken> ExtensionData; + + + /********* + ** Accessors + *********/ + /// <summary>The mod's current unique ID.</summary> + public string ID { get; set; } + + /// <summary>The former mod IDs (if any).</summary> + /// <remarks> + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. Format rules: + /// 1. If the mod's ID changed over time, multiple variants can be separated by the + /// <c>|</c> character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of four manifest fields (ID, Name, Author, and + /// EntryDll) to match. + /// </remarks> + public string FormerIDs { get; set; } + + /// <summary>Maps local versions to a semantic version for update checks.</summary> + public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>(); + + /// <summary>Maps remote versions to a semantic version for update checks.</summary> + public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>(); + + /// <summary>The versioned field data.</summary> + /// <remarks> + /// This maps field names to values. This should be accessed via <see cref="GetFields"/>. + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and <c>Default</c>, separated by pipes (whitespace trimmed). For example, <c>Name</c> + /// will always override the name, <c>Default | Name</c> will only override a blank + /// name, and <c>~1.1 | Default | Name</c> will override blank names up to version 1.1. + /// - The version format is <c>min~max</c> (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a <see cref="ModDataFieldKey"/> value. + /// </remarks> + public IDictionary<string, string> Fields { get; set; } = new Dictionary<string, string>(); + + + /********* + ** Public methods + *********/ + /// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary> + public IEnumerable<ModDataField> GetFields() + { + foreach (KeyValuePair<string, string> pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// <summary>Get a semantic local version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetLocalVersionForUpdateChecks(string version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + /// <summary>Get a semantic remote version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + + /********* + ** Private methods + *********/ + /// <summary>The method invoked after JSON deserialisation.</summary> + /// <param name="context">The deserialisation context.</param> + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (this.ExtensionData != null) + { + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData = null; + } + } + } +} diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs new file mode 100644 index 00000000..332c5c48 --- /dev/null +++ b/src/SMAPI/Framework/ModData/ModDatabase.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.ModData +{ + /// <summary>Handles access to SMAPI's internal mod metadata list.</summary> + internal class ModDatabase + { + /********* + ** Properties + *********/ + /// <summary>The underlying mod data records indexed by default display name.</summary> + private readonly IDictionary<string, ModDataRecord> Records; + + /// <summary>Get an update URL for an update key (if valid).</summary> + private readonly Func<string, string> GetUpdateUrl; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + public ModDatabase() + : this(new Dictionary<string, ModDataRecord>(), key => null) { } + + /// <summary>Construct an instance.</summary> + /// <param name="records">The underlying mod data records indexed by default display name.</param> + /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> + public ModDatabase(IDictionary<string, ModDataRecord> records, Func<string, string> getUpdateUrl) + { + this.Records = records; + this.GetUpdateUrl = getUpdateUrl; + } + + /// <summary>Get a parsed representation of the <see cref="ModDataRecord.Fields"/> which match a given manifest.</summary> + /// <param name="manifest">The manifest to match.</param> + public ParsedModDataRecord GetParsed(IManifest manifest) + { + // get raw record + if (!this.TryGetRaw(manifest, out string displayName, out ModDataRecord record)) + return null; + + // parse fields + ParsedModDataRecord parsed = new ParsedModDataRecord { DisplayName = displayName, DataRecord = record }; + foreach (ModDataField field in record.GetFields().Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; + } + + /// <summary>Get the display name for a given mod ID (if available).</summary> + /// <param name="id">The unique mod ID.</param> + public string GetDisplayNameFor(string id) + { + return this.TryGetRaw(id, out string displayName, out ModDataRecord _) + ? displayName + : null; + } + + /// <summary>Get the mod page URL for a mod (if available).</summary> + /// <param name="id">The unique mod ID.</param> + public string GetModPageUrlFor(string id) + { + // get raw record + if (!this.TryGetRaw(id, out string _, out ModDataRecord record)) + return null; + + // get update key + ModDataField updateKeyField = record.GetFields().FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + if (updateKeyField == null) + return null; + + // get update URL + return this.GetUpdateUrl(updateKeyField.Value); + } + + + /********* + ** Private models + *********/ + /// <summary>Get a raw data record.</summary> + /// <param name="id">The mod ID to match.</param> + /// <param name="displayName">The mod's default display name.</param> + /// <param name="record">The raw mod record.</param> + private bool TryGetRaw(string id, out string displayName, out ModDataRecord record) + { + foreach (var entry in this.Records) + { + if (entry.Value.ID != null && entry.Value.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + { + displayName = entry.Key; + record = entry.Value; + return true; + } + } + + displayName = null; + record = null; + return false; + } + /// <summary>Get a raw data record.</summary> + /// <param name="manifest">The mod manifest whose fields to match.</param> + /// <param name="displayName">The mod's default display name.</param> + /// <param name="record">The raw mod record.</param> + private bool TryGetRaw(IManifest manifest, out string displayName, out ModDataRecord record) + { + if (manifest != null) + { + foreach (var entry in this.Records) + { + displayName = entry.Key; + record = entry.Value; + + // try main ID + if (record.ID != null && record.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + if (record.FormerIDs != null) + { + foreach (string part in record.FormerIDs.Split('|')) + { + // packed field snapshot + if (part.StartsWith("{")) + { + FieldSnapshot snapshot = JsonConvert.DeserializeObject<FieldSnapshot>(part); + bool isMatch = + (snapshot.ID == null || snapshot.ID.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + && (snapshot.EntryDll == null || snapshot.EntryDll.Equals(manifest.EntryDll, StringComparison.InvariantCultureIgnoreCase)) + && ( + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + + if (isMatch) + return true; + } + + // plain ID + else if (part.Equals(manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + } + } + } + + displayName = null; + record = null; + return false; + } + + + /********* + ** Private models + *********/ + /// <summary>A unique set of fields which identifies the mod.</summary> + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialisation.")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialisation.")] + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod ID (or <c>null</c> to ignore it).</summary> + public string ID { get; set; } + + /// <summary>The entry DLL (or <c>null</c> to ignore it).</summary> + public string EntryDll { get; set; } + + /// <summary>The mod name (or <c>null</c> to ignore it).</summary> + public string Name { get; set; } + + /// <summary>The author name (or <c>null</c> to ignore it).</summary> + public string Author { get; set; } + } + } +} diff --git a/src/SMAPI/Framework/Models/ModStatus.cs b/src/SMAPI/Framework/ModData/ModStatus.cs index 343ccb7e..0e1d94d4 100644 --- a/src/SMAPI/Framework/Models/ModStatus.cs +++ b/src/SMAPI/Framework/ModData/ModStatus.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.Models +namespace StardewModdingAPI.Framework.ModData { /// <summary>Indicates how SMAPI should treat a mod.</summary> internal enum ModStatus diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs new file mode 100644 index 00000000..7f49790d --- /dev/null +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -0,0 +1,48 @@ +namespace StardewModdingAPI.Framework.ModData +{ + /// <summary>A parsed representation of the fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary> + internal class ParsedModDataRecord + { + /********* + ** Accessors + *********/ + /// <summary>The underlying data record.</summary> + public ModDataRecord DataRecord { get; set; } + + /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary> + public string DisplayName { get; set; } + + /// <summary>The update key to apply.</summary> + public string UpdateKey { get; set; } + + /// <summary>The alternative URL the player can check for an updated version.</summary> + public string AlternativeUrl { get; set; } + + /// <summary>The predefined compatibility status.</summary> + public ModStatus Status { get; set; } = ModStatus.None; + + /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary> + public string StatusReasonPhrase { get; set; } + + /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Get a semantic local version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetLocalVersionForUpdateChecks(string version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// <summary>Get a semantic remote version for update checks.</summary> + /// <param name="version">The remote version to normalise.</param> + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.DataRecord.GetRemoteVersionForUpdateChecks(version); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4a1d3853..7d8bec1e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -8,6 +8,7 @@ using System.Linq; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Utilities; using StardewValley; using xTile; using xTile.Format; @@ -238,7 +239,7 @@ namespace StardewModdingAPI.Framework.ModHelpers string imageSource = tilesheet.ImageSource; // validate tilesheet path - if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains("..")) + if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); // get seasonal name (if applicable) diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 665b9cf4..b5758d21 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,6 +1,10 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -13,6 +17,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Encapsulates SMAPI's JSON file parsing.</summary> private readonly JsonHelper JsonHelper; + /// <summary>The content packs loaded for this mod.</summary> + private readonly IContentPack[] ContentPacks; + + /// <summary>Create a transitional content pack.</summary> + private readonly Func<string, IManifest, IContentPack> CreateContentPack; + + /// <summary>Manages deprecation warnings.</summary> + private readonly DeprecationManager DeprecationManager; + /********* ** Accessors @@ -48,9 +61,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modRegistry">an API for fetching metadata about loaded mods.</param> /// <param name="reflectionHelper">An API for accessing private game code.</param> /// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param> + /// <param name="contentPacks">The content packs loaded for this mod.</param> + /// <param name="createContentPack">Create a transitional content pack.</param> + /// <param name="deprecationManager">Manages deprecation warnings.</param> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper, IEnumerable<IContentPack> contentPacks, Func<string, IManifest, IContentPack> createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory @@ -67,6 +83,9 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); + this.ContentPacks = contentPacks.ToArray(); + this.CreateContentPack = createContentPack; + this.DeprecationManager = deprecationManager; } /**** @@ -101,7 +120,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public TModel ReadJsonFile<TModel>(string path) where TModel : class { - path = Path.Combine(this.DirectoryPath, path); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFile<TModel>(path); } @@ -112,10 +131,57 @@ namespace StardewModdingAPI.Framework.ModHelpers public void WriteJsonFile<TModel>(string path, TModel model) where TModel : class { - path = Path.Combine(this.DirectoryPath, path); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, model); } + /**** + ** Content packs + ****/ + /// <summary>Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI.</summary> + /// <param name="directoryPath">The absolute directory path containing the content pack files.</param> + /// <param name="id">The content pack's unique ID.</param> + /// <param name="name">The content pack name.</param> + /// <param name="description">The content pack description.</param> + /// <param name="author">The content pack author's name.</param> + /// <param name="version">The content pack version.</param> + [Obsolete("This method supports mods which previously had their own content packs, and shouldn't be used by new mods. It will be removed in SMAPI 3.0.")] + public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) + { + // raise deprecation notice + this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); + + // validate + if(string.IsNullOrWhiteSpace(directoryPath)) + throw new ArgumentNullException(nameof(directoryPath)); + if(string.IsNullOrWhiteSpace(id)) + throw new ArgumentNullException(nameof(id)); + if(string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + if(!Directory.Exists(directoryPath)) + throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); + + // create manifest + IManifest manifest = new Manifest + { + Name = name, + Author = author, + Description = description, + Version = version, + UniqueID = id, + UpdateKeys = new string[0], + ContentPackFor = new ManifestContentPackFor { UniqueID = this.ModID } + }; + + // create content pack + return this.CreateContentPack(directoryPath, manifest); + } + + /// <summary>Get all content packs loaded for this mod.</summary> + public IEnumerable<IContentPack> GetContentPacks() + { + return this.ContentPacks; + } /**** ** Disposal diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index ea0dbb38..e579a830 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private readonly HashSet<string> AccessedModApis = new HashSet<string>(); /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> - private readonly InterfaceProxyBuilder ProxyBuilder; + private readonly InterfaceProxyFactory ProxyFactory; /********* @@ -29,13 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Construct an instance.</summary> /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="registry">The underlying mod registry.</param> - /// <param name="proxyBuilder">Generates proxy classes to access mod APIs through an arbitrary interface.</param> + /// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param> /// <param name="monitor">Encapsulates monitoring and logging for the mod.</param> - public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor) + public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) : base(modID) { this.Registry = registry; - this.ProxyBuilder = proxyBuilder; + this.ProxyFactory = proxyFactory; this.Monitor = monitor; } @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get API of type if (api is TInterface castApi) return castApi; - return this.ProxyBuilder.CreateProxy<TInterface>(api, this.ModID, uniqueID); + return this.ProxyFactory.CreateProxy<TInterface>(api, this.ModID, uniqueID); } } } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 3a7b214a..ccbd053e 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,6 +285,11 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; + case InstructionHandleResult.DetectedUnvalidatedUpdateTick: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); + break; + case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index 0ae598fc..cfa23d08 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Events; + namespace StardewModdingAPI.Framework.ModLoading { /// <summary>Indicates how an instruction was handled.</summary> @@ -19,6 +21,9 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedSaveSerialiser, /// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary> - DetectedDynamic + DetectedDynamic, + + /// <summary>The instruction is compatible, but references <see cref="SpecialisedEvents.UnvalidatedUpdateTick"/> which may impact stability.</summary> + DetectedUnvalidatedUpdateTick } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 30fe211b..1a0f9994 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,4 +1,5 @@ -using StardewModdingAPI.Framework.Models; +using System; +using StardewModdingAPI.Framework.ModData; namespace StardewModdingAPI.Framework.ModLoading { @@ -18,7 +19,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IManifest Manifest { get; } /// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary> - public ModDataRecord DataRecord { get; } + public ParsedModDataRecord DataRecord { get; } /// <summary>The metadata resolution status.</summary> public ModMetadataStatus Status { get; private set; } @@ -26,12 +27,21 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The reason the metadata is invalid, if any.</summary> public string Error { get; private set; } - /// <summary>The mod instance (if it was loaded).</summary> + /// <summary>The mod instance (if loaded and <see cref="IsContentPack"/> is false).</summary> public IMod Mod { get; private set; } + /// <summary>The content pack instance (if loaded and <see cref="IsContentPack"/> is true).</summary> + public IContentPack ContentPack { get; private set; } + + /// <summary>Writes messages to the console and log file as this mod.</summary> + public IMonitor Monitor { get; private set; } + /// <summary>The mod-provided API (if any).</summary> public object Api { get; private set; } + /// <summary>Whether the mod is a content pack.</summary> + public bool IsContentPack => this.Manifest?.ContentPackFor != null; + /********* ** Public methods @@ -41,7 +51,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="directoryPath">The mod's full directory path.</param> /// <param name="manifest">The mod manifest.</param> /// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param> - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ParsedModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; @@ -64,7 +74,24 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="mod">The mod instance to set.</param> public IModMetadata SetMod(IMod mod) { + if (this.ContentPack != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + this.Mod = mod; + this.Monitor = mod.Monitor; + return this; + } + + /// <summary>Set the mod instance.</summary> + /// <param name="contentPack">The contentPack instance to set.</param> + /// <param name="monitor">Writes messages to the console and log file.</param> + public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor) + { + if (this.Mod != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + + this.ContentPack = contentPack; + this.Monitor = monitor; return this; } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 9802d9e9..ba6dab1a 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.ModLoading { @@ -17,12 +19,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Get manifest metadata for each folder in the given root path.</summary> /// <param name="rootPath">The root path to search for mods.</param> /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> - /// <param name="dataRecords">Metadata about mods from SMAPI's internal data.</param> + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <returns>Returns the manifests by relative folder.</returns> - public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable<ModDataRecord> dataRecords) + public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) { - dataRecords = dataRecords.ToArray(); - foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file @@ -31,18 +31,13 @@ namespace StardewModdingAPI.Framework.ModLoading string error = null; try { - // read manifest manifest = jsonHelper.ReadJsonFile<Manifest>(path); - - // validate if (manifest == null) { error = File.Exists(path) ? "its manifest is invalid." : "it doesn't have a manifest."; } - else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - error = "its manifest doesn't set an entry DLL."; } catch (SParseException ex) { @@ -53,26 +48,27 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get internal data record (if any) - ModDataRecord dataRecord = null; - if (manifest != null) + // parse internal data record (if any) + ParsedModDataRecord dataRecord = modDatabase.GetParsed(manifest); + + // get display name + string displayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = dataRecord?.DisplayName; + if (string.IsNullOrWhiteSpace(displayName)) + displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName); + + // apply defaults + if (manifest != null && dataRecord != null) { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); + if (dataRecord.UpdateKey != null) + manifest.UpdateKeys = new[] { dataRecord.UpdateKey }; } - // add default update keys - if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) - manifest.UpdateKeys = dataRecord.UpdateKeys; - // build metadata - string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) - ? manifest.Name - : modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); ModMetadataStatus status = error == null ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); } } @@ -80,8 +76,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Validate manifest metadata.</summary> /// <param name="mods">The mod manifests to validate.</param> /// <param name="apiVersion">The current SMAPI version.</param> - /// <param name="vendorModUrls">Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID).</param> - public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, IDictionary<string, string> vendorModUrls) + /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> + public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string> getUpdateUrl) { mods = mods.ToArray(); @@ -92,42 +88,35 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; - // validate compatibility - ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); - switch (compatibility?.Status) + // validate compatibility from internal data + switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; // get update URLs List<string> updateUrls = new List<string>(); foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) { - string[] parts = key.Split(new[] { ':' }, 2); - if (parts.Length != 2) - continue; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - updateUrls.Add(string.Format(urlTemplate, modID)); + string url = getUpdateUrl(key); + if (url != null) + updateUrls.Add(url); } if (mod.DataRecord.AlternativeUrl != null) updateUrls.Add(mod.DataRecord.AlternativeUrl); // build error string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) error += "newer version"; else - error += $"version newer than {compatibility.UpperVersion}"; + error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); @@ -142,24 +131,52 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // validate DLL value - if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll)) - { - mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field."); - continue; - } - if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + // validate DLL / content pack fields { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); - continue; - } + bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); + bool isContentPack = mod.Manifest.ContentPackFor != null; - // validate DLL path - string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); - continue; + // validate field presence + if (!hasDll && !isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + continue; + } + if (hasDll && isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + continue; + } + + // validate DLL + if (hasDll) + { + // invalid filename format + if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + continue; + } + + // invalid path + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); + if (!File.Exists(assemblyPath)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + } + + // validate content pack + else + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + continue; + } + } } // validate required fields @@ -197,7 +214,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Sort the given mods by the order they should be loaded.</summary> /// <param name="mods">The mods to process.</param> - public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods) + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> + public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods, ModDatabase modDatabase) { // initialise metadata mods = mods.ToArray(); @@ -213,7 +231,7 @@ namespace StardewModdingAPI.Framework.ModLoading // sort mods foreach (IModMetadata mod in mods) - this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List<IModMetadata>()); + this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List<IModMetadata>()); return sortedMods.Reverse(); } @@ -224,12 +242,13 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary> /// <param name="mods">The full list of mods being validated.</param> + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <param name="mod">The mod whose dependencies to process.</param> /// <param name="states">The dependency state for each mod.</param> /// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param> /// <param name="currentChain">The current change of mod dependencies.</param> /// <returns>Returns the mod dependency status.</returns> - private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain) + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary<IModMetadata, ModDependencyStatus> states, Stack<IModMetadata> sortedMods, ICollection<IModMetadata> currentChain) { // check if already visited switch (states[mod]) @@ -255,36 +274,32 @@ namespace StardewModdingAPI.Framework.ModLoading throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } - // no dependencies, mark sorted - if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) + // collect dependencies + ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + + // mark sorted if no dependencies + if (!dependencies.Any()) { sortedMods.Push(mod); return states[mod] = ModDependencyStatus.Sorted; } - // get dependencies - var dependencies = - ( - from entry in mod.Manifest.Dependencies - let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) - orderby entry.UniqueID - select new - { - ID = entry.UniqueID, - MinVersion = entry.MinimumVersion, - Mod = dependencyMod, - IsRequired = entry.IsRequired - } - ) - .ToArray(); - - // missing required dependencies, mark failed + // mark failed if missing dependencies { - string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); - if (failedIDs.Any()) + string[] failedModNames = ( + from entry in dependencies + where entry.IsRequired && entry.Mod == null + let displayName = modDatabase.GetDisplayNameFor(entry.ID) ?? entry.ID + let modUrl = modDatabase.GetModPageUrlFor(entry.ID) + orderby displayName + select modUrl != null + ? $"{displayName}: {modUrl}" + : displayName + ).ToArray(); + if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -329,7 +344,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // recursively process each dependency - var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); + var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); switch (substatus) { // sorted successfully @@ -374,5 +389,64 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } + + /// <summary>Get the dependencies declared in a manifest.</summary> + /// <param name="manifest">The mod manifest.</param> + /// <param name="loadedMods">The loaded mods.</param> + private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + { + IModMetadata FindMod(string id) => loadedMods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, id, StringComparison.InvariantCultureIgnoreCase)); + + // yield dependencies + if (manifest.Dependencies != null) + { + foreach (var entry in manifest.Dependencies) + yield return new ModDependency(entry.UniqueID, entry.MinimumVersion, FindMod(entry.UniqueID), entry.IsRequired); + } + + // yield content pack parent + if (manifest.ContentPackFor != null) + yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true); + } + + + /********* + ** Private models + *********/ + /// <summary>Represents a dependency from one mod to another.</summary> + private struct ModDependency + { + /********* + ** Accessors + *********/ + /// <summary>The unique ID of the required mod.</summary> + public string ID { get; } + + /// <summary>The minimum required version (if any).</summary> + public ISemanticVersion MinVersion { get; } + + /// <summary>Whether the mod shouldn't be loaded if the dependency isn't available.</summary> + public bool IsRequired { get; } + + /// <summary>The loaded mod that fulfills the dependency (if available).</summary> + public IModMetadata Mod { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The unique ID of the required mod.</param> + /// <param name="minVersion">The minimum required version (if any).</param> + /// <param name="mod">The loaded mod that fulfills the dependency (if available).</param> + /// <param name="isRequired">Whether the mod shouldn't be loaded if the dependency isn't available.</param> + public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired) + { + this.ID = id; + this.MinVersion = minVersion; + this.Mod = mod; + this.IsRequired = isRequired; + } + } } } diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 453d2868..e7d4f89a 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -25,18 +25,27 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ - /// <summary>Register a mod as a possible source of deprecation warnings.</summary> + /// <summary>Register a mod.</summary> /// <param name="metadata">The mod metadata.</param> public void Add(IModMetadata metadata) { this.Mods.Add(metadata); - this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; + if (!metadata.IsContentPack) + this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata; } /// <summary>Get metadata for all loaded mods.</summary> - public IEnumerable<IModMetadata> GetAll() + /// <param name="assemblyMods">Whether to include SMAPI mods.</param> + /// <param name="contentPacks">Whether to include content pack mods.</param> + public IEnumerable<IModMetadata> GetAll(bool assemblyMods = true, bool contentPacks = true) { - return this.Mods.Select(p => p); + IEnumerable<IModMetadata> query = this.Mods; + if (!assemblyMods) + query = query.Where(p => p.IsContentPack); + if (!contentPacks) + query = query.Where(p => !p.IsContentPack); + + return query; } /// <summary>Get metadata for a loaded mod.</summary> diff --git a/src/SMAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs index f9762406..f5867cf3 100644 --- a/src/SMAPI/Framework/Models/Manifest.cs +++ b/src/SMAPI/Framework/Models/Manifest.cs @@ -20,16 +20,18 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// <summary>The mod version.</summary> - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion Version { get; set; } /// <summary>The minimum SMAPI version required by this mod, if any.</summary> - [JsonConverter(typeof(SemanticVersionConverter))] public ISemanticVersion MinimumApiVersion { get; set; } - /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method.</summary> + /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary> public string EntryDll { get; set; } + /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="IManifest.EntryDll"/>.</summary> + [JsonConverter(typeof(ManifestContentPackForConverter))] + public IManifestContentPackFor ContentPackFor { get; set; } + /// <summary>The other mods that must be loaded before this mod.</summary> [JsonConverter(typeof(ManifestDependencyArrayConverter))] public IManifestDependency[] Dependencies { get; set; } diff --git a/src/SMAPI/Framework/Models/ManifestContentPackFor.cs b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..7836bbcc --- /dev/null +++ b/src/SMAPI/Framework/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary> + internal class ManifestContentPackFor : IManifestContentPackFor + { + /********* + ** Accessors + *********/ + /// <summary>The unique ID of the mod which can read this content pack.</summary> + public string UniqueID { get; set; } + + /// <summary>The minimum required version (if any).</summary> + public ISemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/SMAPI/Framework/Models/ModCompatibility.cs b/src/SMAPI/Framework/Models/ModCompatibility.cs deleted file mode 100644 index 54737e6c..00000000 --- a/src/SMAPI/Framework/Models/ModCompatibility.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Specifies the compatibility of a given mod version range.</summary> - internal class ModCompatibility - { - /********* - ** Accessors - *********/ - /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> - public ISemanticVersion LowerVersion { get; } - - /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> - public ISemanticVersion UpperVersion { get; } - - /// <summary>The mod compatibility.</summary> - public ModStatus Status { get; } - - /// <summary>The reason phrase to show in log output, or <c>null</c> to use the default value.</summary> - /// <example>For example, "this version is incompatible with the latest version of the game".</example> - public string ReasonPhrase { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="versionRange">A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range.</param> - /// <param name="status">The mod compatibility.</param> - /// <param name="reasonPhrase">The reason phrase to show in log output, or <c>null</c> to use the default value.</param> - public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) - { - // extract version strings - string[] versions = versionRange.Split('~'); - if (versions.Length != 2) - throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); - - // initialise - this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; - this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; - this.Status = status; - this.ReasonPhrase = reasonPhrase; - } - - /// <summary>Get whether a given version is contained within this compatibility range.</summary> - /// <param name="version">The version to check.</param> - public bool MatchesVersion(ISemanticVersion version) - { - return - (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataID.cs b/src/SMAPI/Framework/Models/ModDataID.cs deleted file mode 100644 index d19434fa..00000000 --- a/src/SMAPI/Framework/Models/ModDataID.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Uniquely identifies a mod in SMAPI's internal data.</summary> - /// <remarks> - /// This represents a custom format which uniquely identifies a mod across all versions, even - /// if its field values change or it doesn't specify a unique ID. This is mapped to a string - /// with the following format: - /// - /// 1. If the mod's identifier changed over time, multiple variants can be separated by the <c>|</c> - /// character. - /// 2. Each variant can take one of two forms: - /// - A simple string matching the mod's UniqueID value. - /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. - /// </remarks> - internal class ModDataID - { - /********* - ** Properties - *********/ - /// <summary>The unique sets of field values which identify this mod.</summary> - private readonly FieldSnapshot[] Snapshots; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public ModDataID() { } - - /// <summary>Construct an instance.</summary> - /// <param name="data">The mod identifier string (see remarks on <see cref="ModDataID"/>).</param> - public ModDataID(string data) - { - this.Snapshots = - ( - from string part in data.Split('|') - let str = part.Trim() - select str.StartsWith("{") - ? JsonConvert.DeserializeObject<FieldSnapshot>(str) - : new FieldSnapshot { ID = str } - ) - .ToArray(); - } - - /// <summary>Get whether this ID matches a given mod manifest.</summary> - /// <param name="id">The mod's unique ID, or a substitute ID if it isn't set in the manifest.</param> - /// <param name="manifest">The manifest to check.</param> - public bool Matches(string id, IManifest manifest) - { - return this.Snapshots.Any(snapshot => - snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) - && ( - snapshot.Author == null - || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) - ) - && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) - ); - } - - - /********* - ** Private models - *********/ - /// <summary>A unique set of fields which identifies the mod.</summary> - private class FieldSnapshot - { - /********* - ** Accessors - *********/ - /// <summary>The unique mod ID.</summary> - public string ID { get; set; } - - /// <summary>The mod name, or <c>null</c> to ignore the mod name.</summary> - public string Name { get; set; } - - /// <summary>The author name, or <c>null</c> to ignore the author.</summary> - public string Author { get; set; } - } - } -} diff --git a/src/SMAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs deleted file mode 100644 index 580acb70..00000000 --- a/src/SMAPI/Framework/Models/ModDataRecord.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using StardewModdingAPI.Framework.Serialisation.SmapiConverters; - -namespace StardewModdingAPI.Framework.Models -{ - /// <summary>Metadata about a mod from SMAPI's internal data.</summary> - internal class ModDataRecord - { - /********* - ** Accessors - *********/ - /// <summary>The unique mod identifier.</summary> - [JsonConverter(typeof(ModDataIdConverter))] - public ModDataID ID { get; set; } - - /// <summary>A value to inject into <see cref="IManifest.UpdateKeys"/> field if it's not already set.</summary> - public string[] UpdateKeys { get; set; } - - /// <summary>The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible.</summary> - public string AlternativeUrl { get; set; } - - /// <summary>The compatibility of given mod versions (if any).</summary> - [JsonConverter(typeof(ModCompatibilityArrayConverter))] - public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; - - /// <summary>Map local versions to a semantic version for update checks.</summary> - public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>(); - - /// <summary>Map remote versions to a semantic version for update checks.</summary> - public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>(); - - - /********* - ** Public methods - *********/ - /// <summary>Get the compatibility record for a given version, if any.</summary> - /// <param name="version">The mod version to check.</param> - public ModCompatibility GetCompatibility(ISemanticVersion version) - { - return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); - } - - /// <summary>Get a semantic local version for update checks.</summary> - /// <param name="version">The local version to normalise.</param> - public string GetLocalVersionForUpdateChecks(string version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - - /// <summary>Get a semantic remote version for update checks.</summary> - /// <param name="version">The remote version to normalise.</param> - public string GetRemoteVersionForUpdateChecks(string version) - { - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - } -} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 401e1a3a..17169714 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.ModData; + namespace StardewModdingAPI.Framework.Models { /// <summary>The SMAPI configuration settings.</summary> @@ -22,6 +25,6 @@ namespace StardewModdingAPI.Framework.Models public bool VerboseLogging { get; set; } /// <summary>Extra metadata about mods.</summary> - public ModDataRecord[] ModData { get; set; } + public IDictionary<string, ModDataRecord> ModData { get; set; } } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 5abebc18..7a2958fb 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -1,82 +1,47 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; namespace StardewModdingAPI.Framework.Reflection { - /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> + /// <summary>Generates a proxy class to access a mod API through an arbitrary interface.</summary> internal class InterfaceProxyBuilder { /********* ** Properties *********/ - /// <summary>The CLR module in which to create proxy classes.</summary> - private readonly ModuleBuilder ModuleBuilder; + /// <summary>The target class type.</summary> + private readonly Type TargetType; - /// <summary>The generated proxy types.</summary> - private readonly IDictionary<string, Type> GeneratedTypes = new Dictionary<string, Type>(); + /// <summary>The generated proxy type.</summary> + private readonly Type ProxyType; /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - public InterfaceProxyBuilder() - { - AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); - this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); - } - - /// <summary>Create an API proxy.</summary> - /// <typeparam name="TInterface">The interface through which to access the API.</typeparam> - /// <param name="instance">The API instance to access.</param> - /// <param name="sourceModID">The unique ID of the mod consuming the API.</param> - /// <param name="targetModID">The unique ID of the mod providing the API.</param> - public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) - where TInterface : class + /// <param name="name">The type name to generate.</param> + /// <param name="moduleBuilder">The CLR module in which to create proxy classes.</param> + /// <param name="interfaceType">The interface type to implement.</param> + /// <param name="targetType">The target type.</param> + public InterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) { // 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; - } + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); - // create instance - ConstructorInfo constructor = type.GetConstructor(new[] { targetType }); - if (constructor == null) - throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen - return (TInterface)constructor.Invoke(new[] { instance }); - } - - - /********* - ** Private methods - *********/ - /// <summary>Define a class which proxies access to a target type through an interface.</summary> - /// <param name="proxyTypeName">The name of the proxy type to generate.</param> - /// <param name="interfaceType">The interface type through which to access the target.</param> - /// <param name="targetType">The target type to access.</param> - private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType) - { // define proxy type - TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class); + TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); proxyBuilder.AddInterfaceImplementation(interfaceType); // create field to store target instance - FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); + FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); - // create constructor which accepts target instance + // create constructor which accepts target instance and sets field { ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); ILGenerator il = constructor.GetILGenerator(); @@ -86,7 +51,7 @@ namespace StardewModdingAPI.Framework.Reflection 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.Stfld, targetField); // set field to loaded argument il.Emit(OpCodes.Ret); } @@ -97,13 +62,28 @@ namespace StardewModdingAPI.Framework.Reflection 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); + this.ProxyMethod(proxyBuilder, targetMethod, targetField); } - // create type - return proxyBuilder.CreateType(); + // save info + this.TargetType = targetType; + this.ProxyType = proxyBuilder.CreateType(); + } + + /// <summary>Create an instance of the proxy for a target instance.</summary> + /// <param name="targetInstance">The target instance.</param> + public object CreateInstance(object targetInstance) + { + ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); + if (constructor == null) + throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen + return constructor.Invoke(new[] { targetInstance }); } + + /********* + ** Private methods + *********/ /// <summary>Define a method which proxies access to a method on the target.</summary> /// <param name="proxyBuilder">The proxy type being generated.</param> /// <param name="target">The target method.</param> diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs new file mode 100644 index 00000000..e14a9f08 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> + internal class InterfaceProxyFactory + { + /********* + ** Properties + *********/ + /// <summary>The CLR module in which to create proxy classes.</summary> + private readonly ModuleBuilder ModuleBuilder; + + /// <summary>The generated proxy types.</summary> + private readonly IDictionary<string, InterfaceProxyBuilder> Builders = new Dictionary<string, InterfaceProxyBuilder>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public InterfaceProxyFactory() + { + AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); + } + + /// <summary>Create an API proxy.</summary> + /// <typeparam name="TInterface">The interface through which to access the API.</typeparam> + /// <param name="instance">The API instance to access.</param> + /// <param name="sourceModID">The unique ID of the mod consuming the API.</param> + /// <param name="targetModID">The unique ID of the mod providing the API.</param> + public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) + where TInterface : class + { + // validate + if (instance == null) + throw new InvalidOperationException("Can't proxy access to a null API."); + if (!typeof(TInterface).IsInterface) + throw new InvalidOperationException("The proxy type must be an interface, not a class."); + + // get proxy type + Type targetType = instance.GetType(); + string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; + if (!this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder)) + { + builder = new InterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType); + this.Builders[proxyTypeName] = builder; + } + + // create instance + return (TInterface)builder.CreateInstance(instance); + } + } +} diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index ebea6c84..fa51bd53 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -35,9 +35,6 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ - /// <summary>The preferred directory separator chaeacter in an asset key.</summary> - private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); - /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; @@ -75,9 +72,6 @@ namespace StardewModdingAPI.Framework /// <summary>Interceptors which edit matching assets after they're loaded.</summary> internal IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>(); - /// <summary>The possible directory separator characters in an asset key.</summary> - internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary> internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); @@ -100,7 +94,7 @@ namespace StardewModdingAPI.Framework { // init this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); + this.Cache = new ContentCache(this, reflection); this.GetKeyLocale = reflection.GetMethod(this, "languageCode"); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); @@ -399,15 +393,7 @@ namespace StardewModdingAPI.Framework /// <param name="targetPath">The target file path.</param> private string GetRelativePath(string targetPath) { - // convert to URIs - Uri from = new Uri(this.FullRootDirectory + "/"); - Uri to = new Uri(targetPath + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'."); - - // get relative path - return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) - .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + return PathUtilities.GetRelativePath(this.FullRootDirectory, targetPath); } /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> @@ -581,7 +567,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -608,14 +594,14 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return null; } // validate asset if (data == null) { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); return null; } @@ -644,7 +630,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -657,18 +643,18 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit if (asset.Data == null) { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } else if (!(asset.Data is T)) { - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } } @@ -792,12 +778,12 @@ namespace StardewModdingAPI.Framework { try { - this.Lock.EnterReadLock(); + this.Lock.EnterWriteLock(); return action(); } finally { - this.Lock.ExitReadLock(); + this.Lock.ExitWriteLock(); } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index e82ee778..5c45edca 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; @@ -35,6 +36,9 @@ namespace StardewModdingAPI.Framework /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; + /// <summary>Manages SMAPI events for mods.</summary> + private readonly EventManager Events; + /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary> private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -117,6 +121,9 @@ namespace StardewModdingAPI.Framework /// <summary>The current game instance.</summary> private static SGame Instance; + /// <summary>A callback to invoke after the game finishes initialising.</summary> + private readonly Action OnGameInitialised; + /**** ** Private wrappers ****/ @@ -132,9 +139,9 @@ namespace StardewModdingAPI.Framework set => SGame.Reflection.GetField<float>(typeof(Game1), nameof(_fps)).SetValue(value); } private static Task _newDayTask => SGame.Reflection.GetField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue(); - private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(bgColor)).GetValue(); + private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(this.bgColor)).GetValue(); public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop - public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(this, nameof(lightingBlend)).GetValue(); + public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(this, nameof(this.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(); @@ -158,13 +165,17 @@ namespace StardewModdingAPI.Framework /// <summary>Construct an instance.</summary> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="reflection">Simplifies access to private game code.</param> - internal SGame(IMonitor monitor, Reflector reflection) + /// <param name="eventManager">Manages SMAPI events for mods.</param> + /// <param name="onGameInitialised">A callback to invoke after the game finishes initialising.</param> + internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised) { // initialise this.Monitor = monitor; + this.Events = eventManager; this.FirstUpdate = true; SGame.Instance = this; SGame.Reflection = reflection; + this.OnGameInitialised = onGameInitialised; // set XNA option required by Stardew Valley Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -229,6 +240,7 @@ namespace StardewModdingAPI.Framework if (SGame._newDayTask != null) { base.Update(gameTime); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } @@ -236,6 +248,7 @@ namespace StardewModdingAPI.Framework if (Game1.gameMode == Game1.loadingMode) { base.Update(gameTime); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } @@ -254,19 +267,20 @@ namespace StardewModdingAPI.Framework { this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - SaveEvents.InvokeBeforeCreate(this.Monitor); + this.Events.Save_BeforeCreate.Raise(); } - + // raise before-save if (Context.IsWorldReady && !this.IsBetweenSaveEvents) { this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); + this.Events.Save_BeforeSave.Raise(); } // suppress non-save events base.Update(gameTime); + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); return; } if (this.IsBetweenCreateEvents) @@ -274,24 +288,22 @@ namespace StardewModdingAPI.Framework // raise after-create this.IsBetweenCreateEvents = false; this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterCreated(this.Monitor); + this.Events.Save_AfterCreate.Raise(); } if (this.IsBetweenSaveEvents) { // raise after-save this.IsBetweenSaveEvents = false; this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + this.Events.Save_AfterSave.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } /********* - ** Game loaded events + ** Notify SMAPI that game is initialised *********/ if (this.FirstUpdate) - { - GameEvents.InvokeInitialize(this.Monitor); - } + this.OnGameInitialised(); /********* ** Locale changed events @@ -304,7 +316,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.Events.Content_LocaleChanged.Raise(new EventArgsValueChanged<string>(oldValue.ToString(), newValue.ToString())); + this.PreviousLocale = newValue; } @@ -321,8 +334,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); Context.IsWorldReady = true; - SaveEvents.InvokeAfterLoad(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + this.Events.Save_AfterLoad.Raise(); + this.Events.Time_AfterDayStarted.Raise(); } } @@ -340,7 +353,7 @@ namespace StardewModdingAPI.Framework this.IsExitingToTitle = false; this.CleanupAfterReturnToTitle(); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + this.Events.Save_AfterReturnToTitle.Raise(); } /********* @@ -353,7 +366,7 @@ namespace StardewModdingAPI.Framework if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) { Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); - GraphicsEvents.InvokeResize(this.Monitor); + this.Events.Graphics_Resize.Raise(); this.PreviousWindowSize = size; } @@ -393,47 +406,47 @@ namespace StardewModdingAPI.Framework if (status == InputStatus.Pressed) { - InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + this.Events.Input_ButtonPressed.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - ControlEvents.InvokeKeyPressed(this.Monitor, key); + this.Events.Control_KeyPressed.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right); + this.Events.Control_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); else - ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); + this.Events.Control_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); } } else if (status == InputStatus.Released) { - InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, button.IsActionButton(), button.IsUseToolButton()); + this.Events.Input_ButtonReleased.Raise(new EventArgsInput(button, cursor, button.IsActionButton(), button.IsUseToolButton())); // legacy events if (button.TryGetKeyboard(out Keys key)) { if (key != Keys.None) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + this.Events.Control_KeyReleased.Raise(new EventArgsKeyPressed(key)); } else if (button.TryGetController(out Buttons controllerButton)) { if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right); + this.Events.Control_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.ControllerState.Triggers.Left : inputState.ControllerState.Triggers.Right)); else - ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); + this.Events.Control_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); } } } // raise legacy state-changed events if (inputState.KeyboardState != this.PreviousInput.KeyboardState) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousInput.KeyboardState, inputState.KeyboardState); + this.Events.Control_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(this.PreviousInput.KeyboardState, inputState.KeyboardState)); if (inputState.MouseState != this.PreviousInput.MouseState) - ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition); + this.Events.Control_MouseChanged.Raise(new EventArgsMouseStateChanged(this.PreviousInput.MouseState, inputState.MouseState, this.PreviousInput.MousePosition, inputState.MousePosition)); // track state this.PreviousInput = inputState; @@ -460,9 +473,9 @@ namespace StardewModdingAPI.Framework // raise menu events if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + this.Events.Menu_Changed.Raise(new EventArgsClickableMenuChanged(previousMenu, newMenu)); else - MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); + this.Events.Menu_Closed.Raise(new EventArgsClickableMenuClosed(previousMenu)); // update previous menu // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) @@ -479,46 +492,46 @@ namespace StardewModdingAPI.Framework { if (this.VerboseLogging) this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + this.Events.Location_CurrentLocationChanged.Raise(new EventArgsCurrentLocationChanged(this.PreviousGameLocation, Game1.currentLocation)); } // raise location list changed if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + this.Events.Location_LocationsChanged.Raise(new EventArgsGameLocationsChanged(Game1.locations)); // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { // raise player leveled up a skill if (Game1.player.combatLevel != this.PreviousCombatLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel)); if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel)); if (Game1.player.fishingLevel != this.PreviousFishingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel)); if (Game1.player.foragingLevel != this.PreviousForagingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel)); if (Game1.player.miningLevel != this.PreviousMiningLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel)); if (Game1.player.luckLevel != this.PreviousLuckLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); + this.Events.Player_LeveledUp.Raise(new EventArgsLevelUp(EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel)); // raise player inventory changed ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); if (changedItems.Any()) - PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems); + this.Events.Player_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.items, changedItems.ToList())); // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects)); // raise time changed if (Game1.timeOfDay != this.PreviousTime) - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + this.Events.Time_TimeOfDayChanged.Raise(new EventArgsIntChanged(this.PreviousTime, Game1.timeOfDay)); // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + this.Events.Mine_LevelChanged.Raise(new EventArgsMineLevelChanged(this.PreviousMineLevel, Game1.mine.mineLevel)); } // update state @@ -552,24 +565,25 @@ namespace StardewModdingAPI.Framework /********* ** Update events *********/ + this.Events.Specialised_UnvalidatedUpdateTick.Raise(); if (this.FirstUpdate) { this.FirstUpdate = false; - GameEvents.InvokeFirstUpdateTick(this.Monitor); + this.Events.Game_FirstUpdateTick.Raise(); } - GameEvents.InvokeUpdateTick(this.Monitor); + this.Events.Game_UpdateTick.Raise(); if (this.CurrentUpdateTick % 2 == 0) - GameEvents.InvokeSecondUpdateTick(this.Monitor); + this.Events.Game_SecondUpdateTick.Raise(); if (this.CurrentUpdateTick % 4 == 0) - GameEvents.InvokeFourthUpdateTick(this.Monitor); + this.Events.Game_FourthUpdateTick.Raise(); if (this.CurrentUpdateTick % 8 == 0) - GameEvents.InvokeEighthUpdateTick(this.Monitor); + this.Events.Game_EighthUpdateTick.Raise(); if (this.CurrentUpdateTick % 15 == 0) - GameEvents.InvokeQuarterSecondTick(this.Monitor); + this.Events.Game_QuarterSecondTick.Raise(); if (this.CurrentUpdateTick % 30 == 0) - GameEvents.InvokeHalfSecondTick(this.Monitor); + this.Events.Game_HalfSecondTick.Raise(); if (this.CurrentUpdateTick % 60 == 0) - GameEvents.InvokeOneSecondTick(this.Monitor); + this.Events.Game_OneSecondTick.Raise(); this.CurrentUpdateTick += 1; if (this.CurrentUpdateTick >= 60) this.CurrentUpdateTick = 0; @@ -678,9 +692,9 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -702,9 +716,9 @@ namespace StardewModdingAPI.Framework try { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -769,9 +783,9 @@ namespace StardewModdingAPI.Framework { try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -856,7 +870,7 @@ namespace StardewModdingAPI.Framework Game1.bloom.BeginDraw(); this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); + this.Events.Graphics_OnPreRenderEvent.Raise(); if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -1127,9 +1141,9 @@ namespace StardewModdingAPI.Framework this.drawBillboard(); if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) { - GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); + this.Events.Graphics_OnPreRenderHudEvent.Raise(); this.drawHUD(); - GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); + this.Events.Graphics_OnPostRenderHudEvent.Raise(); } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); @@ -1261,9 +1275,9 @@ namespace StardewModdingAPI.Framework { try { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); } catch (Exception ex) { @@ -1348,11 +1362,11 @@ namespace StardewModdingAPI.Framework /// <param name="needsNewBatch">Whether to create a new sprite batch.</param> private void RaisePostRender(bool needsNewBatch = false) { - if (GraphicsEvents.HasPostRenderListeners()) + if (this.Events.Graphics_OnPostRenderEvent.HasListeners()) { if (needsNewBatch) Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + this.Events.Graphics_OnPostRenderEvent.Raise(); if (needsNewBatch) Game1.spriteBatch.End(); } diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs index f4a2a26e..f1b2f04f 100644 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/ColorConverter.cs @@ -20,10 +20,10 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters /// <param name="path">The path to the current JSON node.</param> protected override Color ReadObject(JObject obj, string path) { - int r = obj.Value<int>(nameof(Color.R)); - int g = obj.Value<int>(nameof(Color.G)); - int b = obj.Value<int>(nameof(Color.B)); - int a = obj.Value<int>(nameof(Color.A)); + int r = obj.ValueIgnoreCase<int>(nameof(Color.R)); + int g = obj.ValueIgnoreCase<int>(nameof(Color.G)); + int b = obj.ValueIgnoreCase<int>(nameof(Color.B)); + int a = obj.ValueIgnoreCase<int>(nameof(Color.A)); return new Color(r, g, b, a); } diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs index 84c70989..434b7ea5 100644 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/PointConverter.cs @@ -20,8 +20,8 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters /// <param name="path">The path to the current JSON node.</param> protected override Point ReadObject(JObject obj, string path) { - int x = obj.Value<int>(nameof(Point.X)); - int y = obj.Value<int>(nameof(Point.Y)); + int x = obj.ValueIgnoreCase<int>(nameof(Point.X)); + int y = obj.ValueIgnoreCase<int>(nameof(Point.Y)); return new Point(x, y); } diff --git a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs index b89551e3..62bc8637 100644 --- a/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs +++ b/src/SMAPI/Framework/Serialisation/CrossplatformConverters/RectangleConverter.cs @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters /// <param name="path">The path to the current JSON node.</param> protected override Rectangle ReadObject(JObject obj, string path) { - int x = obj.Value<int>(nameof(Rectangle.X)); - int y = obj.Value<int>(nameof(Rectangle.Y)); - int width = obj.Value<int>(nameof(Rectangle.Width)); - int height = obj.Value<int>(nameof(Rectangle.Height)); + int x = obj.ValueIgnoreCase<int>(nameof(Rectangle.X)); + int y = obj.ValueIgnoreCase<int>(nameof(Rectangle.Y)); + int width = obj.ValueIgnoreCase<int>(nameof(Rectangle.Width)); + int height = obj.ValueIgnoreCase<int>(nameof(Rectangle.Height)); return new Rectangle(x, y, width, height); } @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework.Serialisation.CrossplatformConverters if (string.IsNullOrWhiteSpace(str)) return Rectangle.Empty; - var match = Regex.Match(str, @"^\{X:(?<x>\d+) Y:(?<y>\d+) Width:(?<width>\d+) Height:(?<height>\d+)\}$"); + var match = Regex.Match(str, @"^\{X:(?<x>\d+) Y:(?<y>\d+) Width:(?<width>\d+) Height:(?<height>\d+)\}$", RegexOptions.IgnoreCase); if (!match.Success) throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); diff --git a/src/SMAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs index 2e2a666e..6cba343e 100644 --- a/src/SMAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/SMAPI/Framework/Serialisation/JsonHelper.cs @@ -21,6 +21,9 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded Converters = new List<JsonConverter> { + // SMAPI types + new SemanticVersionConverter(), + // enums new StringEnumConverter<Buttons>(), new StringEnumConverter<Keys>(), diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs index 3232dde4..af7558f6 100644 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModCompatibilityArrayConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestContentPackForConverter.cs @@ -1,13 +1,11 @@ using System; -using System.Collections.Generic; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters { - /// <summary>Handles deserialisation of <see cref="ModCompatibility"/> arrays.</summary> - internal class ModCompatibilityArrayConverter : JsonConverter + /// <summary>Handles deserialisation of <see cref="IManifestContentPackFor"/> arrays.</summary> + internal class ManifestContentPackForConverter : JsonConverter { /********* ** Accessors @@ -23,7 +21,7 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters /// <param name="objectType">The object type.</param> public override bool CanConvert(Type objectType) { - return objectType == typeof(ModCompatibility[]); + return objectType == typeof(IManifestContentPackFor[]); } @@ -37,16 +35,7 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters /// <param name="serializer">The calling serializer.</param> public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - List<ModCompatibility> result = new List<ModCompatibility>(); - foreach (JProperty property in JObject.Load(reader).Properties()) - { - string range = property.Name; - ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value<string>(nameof(ModCompatibility.Status))); - string reasonPhrase = property.Value.Value<string>(nameof(ModCompatibility.ReasonPhrase)); - - result.Add(new ModCompatibility(range, status, reasonPhrase)); - } - return result.ToArray(); + return serializer.Deserialize<ManifestContentPackFor>(reader); } /// <summary>Writes the JSON representation of the object.</summary> diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs index 6352e367..4150d5fb 100644 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/ManifestDependencyArrayConverter.cs @@ -40,9 +40,9 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters List<IManifestDependency> result = new List<IManifestDependency>(); foreach (JObject obj in JArray.Load(reader).Children<JObject>()) { - string uniqueID = obj.Value<string>(nameof(IManifestDependency.UniqueID)); - string minVersion = obj.Value<string>(nameof(IManifestDependency.MinimumVersion)); - bool required = obj.Value<bool?>(nameof(IManifestDependency.IsRequired)) ?? true; + string uniqueID = obj.ValueIgnoreCase<string>(nameof(IManifestDependency.UniqueID)); + string minVersion = obj.ValueIgnoreCase<string>(nameof(IManifestDependency.MinimumVersion)); + bool required = obj.ValueIgnoreCase<bool?>(nameof(IManifestDependency.IsRequired)) ?? true; result.Add(new ManifestDependency(uniqueID, minVersion, required)); } return result.ToArray(); diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs deleted file mode 100644 index 8a10db47..00000000 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/ModDataIdConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters -{ - /// <summary>Handles deserialisation of <see cref="ModDataID"/>.</summary> - internal class ModDataIdConverter : SimpleReadOnlyConverter<ModDataID> - { - /********* - ** Protected methods - *********/ - /// <summary>Read a JSON string.</summary> - /// <param name="str">The JSON string value.</param> - /// <param name="path">The path to the current JSON node.</param> - protected override ModDataID ReadString(string str, string path) - { - return new ModDataID(str); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs index 50181809..7ee7e29b 100644 --- a/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs +++ b/src/SMAPI/Framework/Serialisation/SmapiConverters/SemanticVersionConverter.cs @@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Serialisation.SmapiConverters /// <param name="path">The path to the current JSON node.</param> protected override ISemanticVersion ReadObject(JObject obj, string path) { - int major = obj.Value<int>(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.Value<int>(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.Value<int>(nameof(ISemanticVersion.PatchVersion)); - string build = obj.Value<string>(nameof(ISemanticVersion.Build)); + int major = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion)); + string build = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.Build)); return new LegacyManifestVersion(major, minor, patch, build); } diff --git a/src/SMAPI/Framework/Utilities/PathUtilities.cs b/src/SMAPI/Framework/Utilities/PathUtilities.cs new file mode 100644 index 00000000..0233d796 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/PathUtilities.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// <summary>Provides utilities for normalising file paths.</summary> + internal static class PathUtilities + { + /********* + ** Properties + *********/ + /// <summary>The possible directory separator characters in a file path.</summary> + private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + + /// <summary>The preferred directory separator chaeacter in an asset key.</summary> + private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); + + + /********* + ** Public methods + *********/ + /// <summary>Get the segments from a path (e.g. <c>/usr/bin/boop</c> => <c>usr</c>, <c>bin</c>, and <c>boop</c>).</summary> + /// <param name="path">The path to split.</param> + public static string[] GetSegments(string path) + { + return path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + /// <summary>Normalise path separators in a file path.</summary> + /// <param name="path">The file path to normalise.</param> + [Pure] + public static string NormalisePathSeparators(string path) + { + string[] parts = PathUtilities.GetSegments(path); + string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); + if (path.StartsWith(PathUtilities.PreferredPathSeparator)) + normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// <summary>Get a directory or file path relative to a given source path.</summary> + /// <param name="sourceDir">The source folder path.</param> + /// <param name="targetPath">The target folder or file path.</param> + [Pure] + public static string GetRelativePath(string sourceDir, string targetPath) + { + // convert to URIs + Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); + + // get relative path + string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + if (relative == "") + relative = "./"; + return relative; + } + } +} diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs new file mode 100644 index 00000000..15a2b7dd --- /dev/null +++ b/src/SMAPI/IContentPack.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using xTile; + +namespace StardewModdingAPI +{ + /// <summary>An API that provides access to a content pack.</summary> + public interface IContentPack + { + /********* + ** Accessors + *********/ + /// <summary>The full path to the content pack's folder.</summary> + string DirectoryPath { get; } + + /// <summary>The content pack's manifest.</summary> + IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Read a JSON file from the content pack folder.</summary> + /// <typeparam name="TModel">The model type.</typeparam> + /// <param name="path">The file path relative to the content pack directory.</param> + /// <returns>Returns the deserialised model, or <c>null</c> if the file doesn't exist or is empty.</returns> + TModel ReadJsonFile<TModel>(string path) where TModel : class; + + /// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> + /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam> + /// <param name="key">The local path to a content file relative to the content pack folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> + T LoadAsset<T>(string key); + + /// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary> + /// <param name="key">The the local path to a content file relative to the content pack folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + string GetActualAssetKey(string key); + } +} diff --git a/src/SMAPI/IManifest.cs b/src/SMAPI/IManifest.cs index 9db1d538..183ac105 100644 --- a/src/SMAPI/IManifest.cs +++ b/src/SMAPI/IManifest.cs @@ -26,9 +26,12 @@ namespace StardewModdingAPI /// <summary>The unique mod ID.</summary> string UniqueID { get; } - /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method.</summary> + /// <summary>The name of the DLL in the directory that has the <see cref="IMod.Entry"/> method. Mutually exclusive with <see cref="EntryDll"/>.</summary> string EntryDll { get; } + /// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="EntryDll"/>.</summary> + IManifestContentPackFor ContentPackFor { get; } + /// <summary>The other mods that must be loaded before this mod.</summary> IManifestDependency[] Dependencies { get; } diff --git a/src/SMAPI/IManifestContentPackFor.cs b/src/SMAPI/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/SMAPI/IManifestContentPackFor.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary> + public interface IManifestContentPackFor + { + /// <summary>The unique ID of the mod which can read this content pack.</summary> + string UniqueID { get; } + + /// <summary>The minimum required version (if any).</summary> + ISemanticVersion MinimumVersion { get; } + } +} diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 116e8508..e9554fdc 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,4 +1,7 @@ -namespace StardewModdingAPI +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI { /// <summary>Provides simplified APIs for writing mods.</summary> public interface IModHelper @@ -54,5 +57,21 @@ /// <param name="path">The file path relative to the mod directory.</param> /// <param name="model">The model to save.</param> void WriteJsonFile<TModel>(string path, TModel model) where TModel : class; + + /**** + ** Content packs + ****/ + /// <summary>Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI.</summary> + /// <param name="directoryPath">The absolute directory path containing the content pack files.</param> + /// <param name="id">The content pack's unique ID.</param> + /// <param name="name">The content pack name.</param> + /// <param name="description">The content pack description.</param> + /// <param name="author">The content pack author's name.</param> + /// <param name="version">The content pack version.</param> + [Obsolete("This method supports mods which previously had their own content packs, and shouldn't be used by new mods. It will be removed in SMAPI 3.0.")] + IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version); + + /// <summary>Get all content packs loaded for this mod.</summary> + IEnumerable<IContentPack> GetContentPacks(); } -}
\ No newline at end of file +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index f285764c..5bb461c1 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -74,6 +74,7 @@ namespace StardewModdingAPI.Metadata new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), + new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick), /**** ** rewrite CIL to fix incompatible code diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7eda9c66..47db8e86 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -17,13 +17,16 @@ using Newtonsoft.Json; using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Utilities; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; @@ -63,7 +66,7 @@ namespace StardewModdingAPI /// <summary>Tracks the installed mods.</summary> /// <remarks>This is initialised after the game starts.</remarks> - private ModRegistry ModRegistry; + private readonly ModRegistry ModRegistry = new ModRegistry(); /// <summary>Manages deprecation warnings.</summary> /// <remarks>This is initialised after the game starts.</remarks> @@ -73,6 +76,9 @@ namespace StardewModdingAPI /// <remarks>This is initialised after the game starts.</remarks> private CommandManager CommandManager; + /// <summary>Manages SMAPI events for mods.</summary> + private readonly EventManager EventManager; + /// <summary>Whether the game is currently running.</summary> private bool IsGameRunning; @@ -86,6 +92,9 @@ namespace StardewModdingAPI new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; + /// <summary>Encapsulates SMAPI's JSON file parsing.</summary> + private readonly JsonHelper JsonHelper = new JsonHelper(); + /********* ** Public methods @@ -123,8 +132,24 @@ namespace StardewModdingAPI /// <param name="logPath">The full file path to which to write log messages.</param> public Program(bool writeToConsole, string logPath) { + // init basics this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole }; + this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + + // hook up events + ContentEvents.Init(this.EventManager); + ControlEvents.Init(this.EventManager); + GameEvents.Init(this.EventManager); + GraphicsEvents.Init(this.EventManager); + InputEvents.Init(this.EventManager); + LocationEvents.Init(this.EventManager); + MenuEvents.Init(this.EventManager); + MineEvents.Init(this.EventManager); + PlayerEvents.Init(this.EventManager); + SaveEvents.Init(this.EventManager); + SpecialisedEvents.Init(this.EventManager); + TimeEvents.Init(this.EventManager); } /// <summary>Launch SMAPI.</summary> @@ -165,7 +190,7 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // override game - this.GameInstance = new SGame(this.Monitor, this.Reflection); + this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -193,7 +218,6 @@ namespace StardewModdingAPI ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); - GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles @@ -255,7 +279,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"The {mod.DisplayName} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); } } @@ -321,7 +345,6 @@ namespace StardewModdingAPI this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components - this.ModRegistry = new ModRegistry(); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); @@ -349,20 +372,23 @@ namespace StardewModdingAPI if (!this.ValidateContentIntegrity()) this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); + // load mod data + ModDatabase modDatabase = new ModDatabase(this.Settings.ModData, Constants.GetUpdateUrl); + // load mods { this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray(); + resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.GetUpdateUrl); // process dependencies - mods = resolver.ProcessDependencies(mods).ToArray(); + mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods - this.LoadMods(mods, new JsonHelper(), this.ContentManager); + this.LoadMods(mods, this.JsonHelper, this.ContentManager); // check for updates this.CheckForUpdatesAsync(mods); @@ -390,7 +416,7 @@ namespace StardewModdingAPI LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetAll()) + foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); } @@ -648,20 +674,57 @@ namespace StardewModdingAPI { this.Monitor.Log("Loading mods...", LogLevel.Trace); - // load mod assemblies IDictionary<IModMetadata, string> skippedMods = new Dictionary<IModMetadata, string>(); + void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + + // load content packs + foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) { - void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + // get basic info + IManifest manifest = metadata.Manifest; + this.Monitor.Log($"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)} (content pack)...", LogLevel.Trace); + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); + TrackSkip(metadata, metadata.Error); + continue; + } + + // load mod as content pack + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); + metadata.SetMod(contentPack, monitor); + this.ModRegistry.Add(metadata); + } + IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); + + // load mods + { + // get content packs by mod ID + IDictionary<string, IContentPack[]> contentPacksByModID = + loadedContentPacks + .GroupBy(p => p.Manifest.ContentPackFor.UniqueID) + .ToDictionary( + group => group.Key, + group => group.Select(metadata => metadata.ContentPack).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + + // get assembly loaders 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) + InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); + + // load from metadata + foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack)) { // get basic info IManifest manifest = metadata.Manifest; this.Monitor.Log(metadata.Manifest?.EntryDll != null - ? $"Loading {metadata.DisplayName} from {metadata.DirectoryPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid + ? $"Loading {metadata.DisplayName} from {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid : $"Loading {metadata.DisplayName}...", LogLevel.Trace); // validate status @@ -672,14 +735,14 @@ namespace StardewModdingAPI continue; } - // preprocess & load mod assembly + // load mod string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) : null; Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { @@ -700,6 +763,10 @@ namespace StardewModdingAPI // initialise mod try { + // get content packs + if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks)) + contentPacks = new IContentPack[0]; + // init mod helpers IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IModHelper modHelper; @@ -707,9 +774,17 @@ 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, proxyBuilder, monitor); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, 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); + + IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); + } + + modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager); } // get mod instance @@ -731,7 +806,7 @@ namespace StardewModdingAPI } } } - IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); // log skipped mods this.Monitor.Newline(); @@ -753,6 +828,7 @@ namespace StardewModdingAPI // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; @@ -765,10 +841,30 @@ namespace StardewModdingAPI } this.Monitor.Newline(); + // log loaded content packs + if (loadedContentPacks.Any()) + { + string GetModDisplayName(string id) => loadedMods.First(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + + this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); + foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + } + // initialise translations - this.ReloadTranslations(); + this.ReloadTranslations(loadedMods); - // initialise loaded mods + // initialise loaded non-content-pack mods foreach (IModMetadata metadata in loadedMods) { // add interceptors @@ -793,13 +889,19 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); + metadata.LogAsMod($"Mod crashed 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 && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } + if (api != null) this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); metadata.SetApi(api); @@ -881,11 +983,15 @@ namespace StardewModdingAPI } /// <summary>Reload translations for all mods.</summary> - private void ReloadTranslations() + /// <param name="mods">The mods for which to reload translations.</param> + private void ReloadTranslations(IEnumerable<IModMetadata> mods) { JsonHelper jsonHelper = new JsonHelper(); - foreach (IModMetadata metadata in this.ModRegistry.GetAll()) + foreach (IModMetadata metadata in mods) { + if (metadata.IsContentPack) + throw new InvalidOperationException("Can't reload translations for a content pack."); + // read translation files IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>(); DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); @@ -900,11 +1006,29 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}"); } } } + // validate translations + foreach (string locale in translations.Keys) + { + HashSet<string> keys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + HashSet<string> duplicateKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in translations[locale].Keys.ToArray()) + { + if (!keys.Add(key)) + { + duplicateKeys.Add(key); + translations[locale].Remove(key); + } + } + + if (duplicateKeys.Any()) + metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); + } + // update translation TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; translationHelper.SetTranslations(translations); @@ -944,7 +1068,7 @@ namespace StardewModdingAPI break; case "reload_i18n": - this.ReloadTranslations(); + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs index b0a065f5..03843ea8 100644 --- a/src/SMAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -1,9 +1,7 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -[assembly: AssemblyTitle("Stardew Modding API (SMAPI)")] +[assembly: AssemblyTitle("SMAPI")] [assembly: AssemblyDescription("A modding API for Stardew Valley.")] -[assembly: Guid("5c3f7f42-fefd-43db-aaea-92ea3bcad531")] [assembly: InternalsVisibleTo("StardewModdingAPI.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 18a9f978..c7527275 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -39,2041 +39,1792 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "VerboseLogging": false, /** - * Extra metadata about some SMAPI mods. All fields except 'ID' are optional. + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. * - * - 'ID' uniquely identifies the mod across all versions, even if its manifest fields changed or - * the mod doesn't have a unique ID. The format is as follows: - * - If the mod's identifier changed over time, multiple variants are separated by |. - * - Each variant can take one of two forms: a simple string matching the mod's UniqueID, - * or a JSON structure containing any of three manifest fields (ID, Name, and Author) to - * match. + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). * - * - 'UpdateKeys' specifies the value of the equivalent manifest field if it's not already set. - * This is used to enable update checks for older mods that haven't been updated to use it yet. + * - ID: the mod's latest unique ID (if any). * - * - 'AlternativeUrl' specifies a URL where the player can find an unofficial update or - * alternative if the mod is no longer compatible. + * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching + * other fields if no ID was specified. This doesn't include the latest ID, if any. + * Format rules: + * 1. If the mod's ID changed over time, multiple variants can be separated by the '|' + * character. + * 2. Each variant can take one of two forms: + * - A simple string matching the mod's UniqueID value. + * - A JSON structure containing any of four manifest fields (ID, Name, Author, and + * EntryDll) to match. * - * - 'Compatibility' overrides SMAPI's normal compatibility detection. The keys are version - * ranges in the form lower~upper, where either side can be blank for an unbounded range. (For - * example, "~1.0" means all versions up to 1.0 inclusively.) The values have two fields: - * - 'Status' specifies the compatibility. Valid values are Obsolete (SMAPI won't load it - * because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will load it even - * if it detects incompatible code). - * - 'ReasonPhrase' (optional) specifies a message to show to the player explaining why the - * mod isn't loaded. This has no effect for AssumeCompatible. + * - MapLocalVersions and MapRemoteVersions crrect local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. * - * - 'MapLocalVersions' and 'MapRemoteVersions' substitute versions for update checks. For - * example, if the API returns version '1.1-1078', MapRemoteVersions can map it to '1.1' when - * comparing to the mod's current version. This is only intended to support legacy mods with - * injected update keys. + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. */ - "ModData": [ - { - // AccessChestAnywhere + "ModData": { + "AccessChestAnywhere": { "ID": "AccessChestAnywhere", - "UpdateKeys": [ "Nexus:257" ], - "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 - }, - "MapLocalVersions": { - "1.1-1078": "1.1" - } - }, - { - // AdjustArtisanPrices + "MapLocalVersions": { "1.1-1078": "1.1" }, + "Default | UpdateKey": "Nexus:257", + "~1.1 | Status": "AssumeBroken", + "~1.1 | AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "AdjustArtisanPrices": { "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", - "UpdateKeys": [ "Chucklefish:3532" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 - } - }, - { - // Adjust Monster + "Default | UpdateKey": "Chucklefish:3532", + "~0.1 | Status": "AssumeBroken", + "~0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Adjust Monster": { "ID": "mmanlapat.AdjustMonster", - "UpdateKeys": [ "Nexus:1161" ] + "Default | UpdateKey": "Nexus:1161" }, - { - // Advanced Location Loader + + "Advanced Location Loader": { "ID": "Entoarox.AdvancedLocationLoader", - //"UpdateKeys": [ "Chucklefish:3619" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.3.7 | UpdateKey": "Chucklefish:3619", // only enable update checks up to 1.3.7 by request (has its own update-check feature) + "~1.2.10 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Adventure Shop Inventory + + "Adventure Shop Inventory": { "ID": "HammurabiAdventureShopInventory", - "UpdateKeys": [ "Chucklefish:4608" ] + "Default | UpdateKey": "Chucklefish:4608" }, - { - // AgingMod + + "AgingMod": { "ID": "skn.AgingMod", - "UpdateKeys": [ "Nexus:1129" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // All Crops All Seasons - "ID": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 - "UpdateKeys": [ "Nexus:170" ] - }, - { - // All Professions - "ID": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 - "UpdateKeys": [ "Nexus:174" ] - }, - { - // Almighty Tool - "ID": "AlmightyTool.dll | 439", // changed in 1.2.1 - "UpdateKeys": [ "Nexus:439" ], - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapRemoteVersions": { - "1.21": "1.2.1" - } - }, - { - // Animal Mood Fix + "Default | UpdateKey": "Nexus:1129", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "All Crops All Seasons": { + "ID": "cantorsdust.AllCropsAllSeasons", + "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5 + "Default | UpdateKey": "Nexus:170" + }, + + "All Professions": { + "ID": "cantorsdust.AllProfessions", + "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:174" + }, + + "Almighty Tool": { + "ID": "439", + "FormerIDs": "{EntryDll: 'AlmightyTool.dll'}", // changed in 1.2.1 + "MapRemoteVersions": { "1.21": "1.2.1" }, + "Default | UpdateKey": "Nexus:439", + "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Animal Husbandry": { + "ID": "DIGUS.ANIMALHUSBANDRYMOD", + "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1 + "Default | UpdateKey": "Nexus:1538" + }, + + "Animal Mood Fix": { "ID": "GPeters-AnimalMoodFix", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - } - } - }, - { - // Animal Sitter - "ID": "AnimalSitter.dll | jwdred.AnimalSitter", // changed in 1.0.9 - "UpdateKeys": [ "Nexus:581" ], - "Compatibility": { - "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // A Tapper's Dream + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + + "Animal Sitter": { + "ID": "jwdred.AnimalSitter", + "FormerIDs": "{EntryDll: 'AnimalSitter.dll'}", // changed in 1.0.9 + "Default | UpdateKey": "Nexus:581", + "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "A Tapper's Dream": { "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "UpdateKeys": [ "Nexus:260" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Auto Animal Doors + "Default | UpdateKey": "Nexus:260", + "~1.4 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.4 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Auto Animal Doors": { "ID": "AaronTaggart.AutoAnimalDoors", - "UpdateKeys": [ "Nexus:1019" ], - "MapRemoteVersions": { - "1.1.1": "1.1" // manifest not updated - } - }, - { - // Auto-Eat - "ID": "BALANCEMOD_AutoEat | Permamiss.AutoEat", // changed in 1.1.1 - "UpdateKeys": [ "Nexus:643" ] - }, - { - // AutoGate + "MapRemoteVersions": { "1.1.1": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1019" + }, + + "Auto-Eat": { + "ID": "Permamiss.AutoEat", + "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1 + "Default | UpdateKey": "Nexus:643" + }, + + "AutoGate": { "ID": "AutoGate", - "UpdateKeys": [ "Nexus:820" ] + "Default | UpdateKey": "Nexus:820" }, - { - // Automate + + "Automate": { "ID": "Pathoschild.Automate", - "UpdateKeys": [ "Nexus:1063" ] - }, - { - // Automated Doors - "ID": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b | azah.automated-doors", // changed in 1.4.1 - "UpdateKeys": [ "GitHub:azah/AutomatedDoors" ], - "MapLocalVersions": { - "1.4.1-1": "1.4.1" - } - }, - { - // AutoSpeed - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'} | Omegasis.AutoSpeed", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:443" ] // added in 1.4.1 - }, - { - // Basic Sprinkler Improved + "Default | UpdateKey": "Nexus:1063" + }, + + "Automated Doors": { + "ID": "azah.automated-doors", + "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 + "MapLocalVersions": { "1.4.1-1": "1.4.1" }, + "Default | UpdateKey": "GitHub:azah/AutomatedDoors" + }, + + "AutoSpeed": { + "ID": "Omegasis.AutoSpeed", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:443" // added in 1.4.1 + }, + + "Basic Sprinklers Improved": { "ID": "lrsk_sdvm_bsi.0117171308", - "UpdateKeys": [ "Nexus:833" ], - "MapRemoteVersions": { - "1.0.2": "1.0.1-release" // manifest not updated - } + "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:833" }, - { - // Better Hay + + "Better Hay": { "ID": "cat.betterhay", - "UpdateKeys": [ "Nexus:1430" ] + "Default | UpdateKey": "Nexus:1430" }, - { - // Better Quality More Seasons + + "Better Quality More Seasons": { "ID": "SB_BQMS", - "UpdateKeys": [ "Nexus:935" ] + "Default | UpdateKey": "Nexus:935" }, - { - // Better Quarry + + "Better Quarry": { "ID": "BetterQuarry", - "UpdateKeys": [ "Nexus:771" ] + "Default | UpdateKey": "Nexus:771" }, - { - // Better Ranching + + "Better Ranching": { "ID": "BetterRanching", - "UpdateKeys": [ "Nexus:859" ] + "Default | UpdateKey": "Nexus:859" }, - { - // Better Shipping Box + + "Better Shipping Box": { "ID": "Kithio:BetterShippingBox", - "UpdateKeys": [ "Chucklefish:4302" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.0.1": "1.0.2" - } - }, - { - // Better Sprinklers - "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 - "UpdateKeys": [ "Nexus:41" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Billboard Anywhere - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'} | Omegasis.BillboardAnywhere", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:492" ] // added in 1.4.1 - }, - { - // Birthday Mail - "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d | KathrynHazuka.BirthdayMail", // changed in 1.2.3-pathoschild-update - "UpdateKeys": [ "Nexus:276" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Breed Like Rabbits + "MapLocalVersions": { "1.0.1": "1.0.2" }, + "Default | UpdateKey": "Chucklefish:4302", + "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "FormerIDs": "SPDSprinklersMod", // changed in 2.3 + "Default | UpdateKey": "Nexus:41", + "~2.3.1-pathoschild-update | Status": "AssumeBroken", // broke in SDV 1.2 + "~2.3.1-pathoschild-update | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Billboard Anywhere": { + "ID": "Omegasis.BillboardAnywhere", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:492" // added in 1.4.1 + }, + + "Birthday Mail": { + "ID": "KathrynHazuka.BirthdayMail", + "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update + "Default | UpdateKey": "Nexus:276", + "~1.2.2 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Breed Like Rabbits": { "ID": "dycedarger.breedlikerabbits", - "UpdateKeys": [ "Nexus:948" ] - }, - { - // Build Endurance - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'} | Omegasis.BuildEndurance", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:445" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Build Health - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'} | Omegasis.BuildHealth", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:446" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Butcher Mod - "ID": "DIGUS.BUTCHER", - "UpdateKeys": [ "Nexus:1538" ] - }, - { - // Buy Cooking Recipes + "Default | UpdateKey": "Nexus:948" + }, + + "Build Endurance": { + "ID": "Omegasis.BuildEndurance", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:445", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Build Health": { + "ID": "Omegasis.BuildHealth", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:446", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Buy Cooking Recipes": { "ID": "Denifia.BuyRecipes", - "UpdateKeys": [ "Nexus:1126" ], // added in 1.0.1 (2017-10-04) - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Buy Back Collectables - "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 - "UpdateKeys": [ "Nexus:507" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Carry Chest + "Default | UpdateKey": "Nexus:1126", // added in 1.0.1 (2017-10-04) + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Buy Back Collectables": { + "ID": "Omegasis.BuyBackCollectables", + "FormerIDs": "BuyBackCollectables", // changed in 1.4 + "Default | UpdateKey": "Nexus:507", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Carry Chest": { "ID": "spacechase0.CarryChest", - "UpdateKeys": [ "Nexus:1333" ] + "Default | UpdateKey": "Nexus:1333" }, - { - // Casks Anywhere + + "Casks Anywhere": { "ID": "CasksAnywhere", - "UpdateKeys": [ "Nexus:878" ], - "MapLocalVersions": { - "1.1-alpha": "1.1" - } + "MapLocalVersions": { "1.1-alpha": "1.1" }, + "Default | UpdateKey": "Nexus:878" }, - { - // Categorize Chests + + "Categorize Chests": { "ID": "CategorizeChests", - "UpdateKeys": [ "Nexus:1300" ] + "Default | UpdateKey": "Nexus:1300" }, - { - // ChefsCloset + + "Chefs Closet": { "ID": "Duder.ChefsCloset", - "UpdateKeys": [ "Nexus:1030" ], - "MapLocalVersions": { - "1.3-1": "1.3" - } - }, - { - // Chest Label System - "ID": "SPDChestLabel | Speeder.ChestLabel", // changed in 1.5.1-pathoschild-update - "UpdateKeys": [ "Nexus:242" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // Chest Pooling - "ID": "ChestPooling.dll | mralbobo.ChestPooling", // changed in 1.3 - "UpdateKeys": [ "GitHub:mralbobo/stardew-chest-pooling" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Chests Anywhere - "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 - "UpdateKeys": [ "Nexus:518" ], - "Compatibility": { - "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Choose Baby Gender - "ID": "ChooseBabyGender.dll", - "UpdateKeys": [ "Nexus:590" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // CJB Automation + "MapLocalVersions": { "1.3-1": "1.3" }, + "Default | UpdateKey": "Nexus:1030" + }, + + "Chest Label System": { + "ID": "Speeder.ChestLabel", + "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update + "Default | UpdateKey": "Nexus:242", + "~1.6 | Status": "AssumeBroken", // broke in SDV 1.1 + "~1.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Chest Pooling": { + "ID": "mralbobo.ChestPooling", + "FormerIDs": "{EntryDll: 'ChestPooling.dll'}", // changed in 1.3 + "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Chests Anywhere": { + "ID": "Pathoschild.ChestsAnywhere", + "FormerIDs": "ChestsAnywhere", // changed in 1.9 + "Default | UpdateKey": "Nexus:518", + "~1.9-beta | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Choose Baby Gender": { + "ID": "{EntryDll: 'ChooseBabyGender.dll'}", + "Default | UpdateKey": "Nexus:590", + "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "CJB Automation": { "ID": "CJBAutomation", - "UpdateKeys": [ "Nexus:211" ], - "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // CJB Cheats Menu - "ID": "CJBCheatsMenu | CJBok.CheatsMenu", // changed in 1.14 - "UpdateKeys": [ "Nexus:4" ], - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // CJB Item Spawner - "ID": "CJBItemSpawner | CJBok.ItemSpawner", // changed in 1.7 - "UpdateKeys": [ "Nexus:93" ], - "Compatibility": { - "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // CJB Show Item Sell Price - "ID": "CJBShowItemSellPrice | CJBok.ShowItemSellPrice", // changed in 1.7 - "UpdateKeys": [ "Nexus:5" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Clean Farm + "Default | UpdateKey": "Nexus:211", + "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063" + }, + + "CJB Cheats Menu": { + "ID": "CJBok.CheatsMenu", + "FormerIDs": "CJBCheatsMenu", // changed in 1.14 + "Default | UpdateKey": "Nexus:4", + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "CJB Item Spawner": { + "ID": "CJBok.ItemSpawner", + "FormerIDs": "CJBItemSpawner", // changed in 1.7 + "Default | UpdateKey": "Nexus:93", + "~1.5 | Status": "AssumeBroken" // broke in SDV 1.1 + }, + + "CJB Show Item Sell Price": { + "ID": "CJBok.ShowItemSellPrice", + "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7 + "Default | UpdateKey": "Nexus:5", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Clean Farm": { "ID": "tstaples.CleanFarm", - "UpdateKeys": [ "Nexus:794" ] + "Default | UpdateKey": "Nexus:794" }, - { - // Climates of Ferngill + + "Climates of Ferngill": { "ID": "KoihimeNakamura.ClimatesOfFerngill", - "UpdateKeys": [ "Nexus:604" ], - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:604", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Cold Weather Haley + + "Cold Weather Haley": { "ID": "LordXamon.ColdWeatherHaleyPRO", - "UpdateKeys": [ "Nexus:1169" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Colored Chests + "Default | UpdateKey": "Nexus:1169", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Colored Chests": { "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." - } - } - }, - { - // Combat with Farm Implements + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + + "Combat with Farm Implements": { "ID": "SPDFarmingImplementsInCombat", - "UpdateKeys": [ "Nexus:313" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Community Bundle Item Tooltip + "Default | UpdateKey": "Nexus:313", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Community Bundle Item Tooltip": { "ID": "musbah.bundleTooltip", - "UpdateKeys": [ "Nexus:1329" ] + "Default | UpdateKey": "Nexus:1329" }, - { - // Concentration on Farming + + "Concentration on Farming": { "ID": "punyo.ConcentrationOnFarming", - "UpdateKeys": [ "Nexus:1445" ] + "Default | UpdateKey": "Nexus:1445" }, - { - // Configurable Machines + + "Configurable Machines": { "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "UpdateKeys": [ "Nexus:280" ], - "MapLocalVersions": { - "1.2-beta": "1.2" - } + "MapLocalVersions": { "1.2-beta": "1.2" }, + "Default | UpdateKey": "Nexus:280" }, - { - // Configurable Shipping Dates + + "Configurable Shipping Dates": { "ID": "ConfigurableShippingDates", - "UpdateKeys": [ "Nexus:675" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Cooking Skill - "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 - "UpdateKeys": [ "Nexus:522" ], - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // CrabNet - "ID": "CrabNet.dll | jwdred.CrabNet", // changed in 1.0.5 - "UpdateKeys": [ "Nexus:584" ], - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Current Location + "Default | UpdateKey": "Nexus:675", + "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Cooking Skill": { + "ID": "spacechase0.CookingSkill", + "FormerIDs": "CookingSkill", // changed in 1.0.4–6 + "Default | UpdateKey": "Nexus:522", + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "CrabNet": { + "ID": "jwdred.CrabNet", + "FormerIDs": "{EntryDll: 'CrabNet.dll'}", // changed in 1.0.5 + "Default | UpdateKey": "Nexus:584", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Current Location": { "ID": "CurrentLocation102120161203", - "UpdateKeys": [ "Nexus:638" ] + "Default | UpdateKey": "Nexus:638" }, - { - // Custom Critters + + "Custom Critters": { "ID": "spacechase0.CustomCritters", - "UpdateKeys": [ "Nexus:1255" ] + "Default | UpdateKey": "Nexus:1255" }, - { - // Custom Element Handler + + "Custom Crops": { + "ID": "spacechase0.CustomCrops", + "Default | UpdateKey": "Nexus:1592" + }, + + "Custom Element Handler": { "ID": "Platonymous.CustomElementHandler", - "UpdateKeys": [ "Nexus:1068" ] // added in 1.3.1 + "Default | UpdateKey": "Nexus:1068" // added in 1.3.1 }, - { - // Custom Farming + + "Custom Farming Redux": { "ID": "Platonymous.CustomFarming", - "UpdateKeys": [ "Nexus:991" ] // added in 0.6.1 + "Default | UpdateKey": "Nexus:991" // added in 0.6.1 }, - { - // Custom Farming Automate Bridge + + "Custom Farming Automate Bridge": { "ID": "Platonymous.CFAutomate", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // no longer compatible with Automate - }, - "AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" + "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate + "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991" }, - { - // Custom Farm Types + + "Custom Farm Types": { "ID": "spacechase0.CustomFarmTypes", - "UpdateKeys": [ "Nexus:1140" ] + "Default | UpdateKey": "Nexus:1140" }, - { - // Custom Furniture + + "Custom Furniture": { "ID": "Platonymous.CustomFurniture", - "UpdateKeys": [ "Nexus:1254" ] // added in 0.4.1 - }, - { - // Customize Exterior - "ID": "CustomizeExterior | spacechase0.CustomizeExterior", // changed in 1.0.3 - "UpdateKeys": [ "Nexus:1099" ], - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Customizable Cart Redux + "Default | UpdateKey": "Nexus:1254" // added in 0.4.1 + }, + + "Customize Exterior": { + "ID": "spacechase0.CustomizeExterior", + "FormerIDs": "CustomizeExterior", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:1099", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + + "Customizable Cart Redux": { "ID": "KoihimeNakamura.CCR", - "UpdateKeys": [ "Nexus:1402" ], - "MapLocalVersions": { - "1.1-20170917": "1.1" - } + "MapLocalVersions": { "1.1-20170917": "1.1" }, + "Default | UpdateKey": "Nexus:1402" }, - { - // Customizable Traveling Cart Days + + "Customizable Traveling Cart Days": { "ID": "TravelingCartYyeahdude", - "UpdateKeys": [ "Nexus:567" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Custom Linens + "Default | UpdateKey": "Nexus:567", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Custom Linens": { "ID": "Mevima.CustomLinens", - "UpdateKeys": [ "Nexus:1027" ], - "MapRemoteVersions": { - "1.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1027" }, - { - // Custom Shops Redux + + "Custom NPC": { + "ID": "Platonymous.CustomNPC", + "Default | UpdateKey": "Nexus:1607" + }, + + "Custom Shops Redux": { "ID": "Omegasis.CustomShopReduxGui", - "UpdateKeys": [ "Nexus:1378" ] // added in 1.4.1 + "Default | UpdateKey": "Nexus:1378" // added in 1.4.1 }, - { - // Custom TV + + "Custom TV": { "ID": "Platonymous.CustomTV", - "UpdateKeys": [ "Nexus:1139" ] // added in 1.0.6 + "Default | UpdateKey": "Nexus:1139" // added in 1.0.6 }, - { - // Daily Luck Message + + "Daily Luck Message": { "ID": "Schematix.DailyLuckMessage", - "UpdateKeys": [ "Nexus:1327" ] + "Default | UpdateKey": "Nexus:1327" }, - { - // Daily News + + "Daily News": { "ID": "bashNinja.DailyNews", - "UpdateKeys": [ "Nexus:1141" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Daily Quest Anywhere - "ID": "DailyQuest | Omegasis.DailyQuestAnywhere", // changed in 1.4 - "UpdateKeys": [ "Nexus:513" ] // added in 1.4.1 - }, - { - // Debug Mode - "ID": "Pathoschild.Stardew.DebugMode | Pathoschild.DebugMode", // changed in 1.4 - "UpdateKeys": [ "Nexus:679" ] - }, - { - // Dynamic Checklist + "Default | UpdateKey": "Nexus:1141", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Daily Quest Anywhere": { + "ID": "Omegasis.DailyQuestAnywhere", + "FormerIDs": "DailyQuest", // changed in 1.4 + "Default | UpdateKey": "Nexus:513" // added in 1.4.1 + }, + + "Debug Mode": { + "ID": "Pathoschild.DebugMode", + "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4 + "Default | UpdateKey": "Nexus:679" + }, + + "Dynamic Checklist": { "ID": "gunnargolf.DynamicChecklist", - "UpdateKeys": [ "Nexus:1145" ], // added in 1.0.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Dynamic Horses + "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Dynamic Horses": { "ID": "Bpendragon-DynamicHorses", - "UpdateKeys": [ "Nexus:874" ], - "MapRemoteVersions": { - "1.2": "1.1-release" // manifest not updated - } + "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated + "Default | UpdateKey": "Nexus:874" }, - { - // Dynamic Machines + + "Dynamic Machines": { "ID": "DynamicMachines", - "UpdateKeys": [ "Nexus:374" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.1": "1.1.1" - } - }, - { - // Dynamic NPC Sprites + "MapLocalVersions": { "1.1": "1.1.1" }, + "Default | UpdateKey": "Nexus:374", + "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Dynamic NPC Sprites": { "ID": "BashNinja.DynamicNPCSprites", - "UpdateKeys": [ "Nexus:1183" ] + "Default | UpdateKey": "Nexus:1183" }, - { - // Easier Farming + + "Easier Farming": { "ID": "cautiouswafffle.EasierFarming", - "UpdateKeys": [ "Nexus:1426" ] + "Default | UpdateKey": "Nexus:1426" }, - { - // Empty Hands + + "Empty Hands": { "ID": "QuicksilverFox.EmptyHands", - "UpdateKeys": [ "Nexus:1176" ], // added in 1.0.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Enemy Health Bars - "ID": "SPDHealthBar | Speeder.HealthBars", // changed in 1.7.1-pathoschild-update - "UpdateKeys": [ "Nexus:193" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Entoarox Framework - "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - //"UpdateKeys": [ "Chucklefish:4228" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Expanded Fridge / Dynamic Expanded Fridge + "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Enemy Health Bars": { + "ID": "Speeder.HealthBars", + "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update + "Default | UpdateKey": "Nexus:193", + "~1.7 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.7 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Entoarox Framework": { + "ID": "Entoarox.EntoaroxFramework", + "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ??? + "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature) + "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals) + }, + + "Expanded Fridge": { "ID": "Uwazouri.ExpandedFridge", - "UpdateKeys": [ "Nexus:1191" ] + "Default | UpdateKey": "Nexus:1191" }, - { - // Experience Bars - "ID": "ExperienceBars | spacechase0.ExperienceBars", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:509" ] + + "Experience Bars": { + "ID": "spacechase0.ExperienceBars", + "FormerIDs": "ExperienceBars", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:509" }, - { - // Extended Bus System + + "Extended Bus System": { "ID": "ExtendedBusSystem", - "UpdateKeys": [ "Chucklefish:4373" ] - }, - { - // Extended Fridge - "ID": "Mystra007ExtendedFridge | Crystalmir.ExtendedFridge", // changed in 1.0.1 - "UpdateKeys": [ "Nexus:485" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Extended Greenhouse + "Default | UpdateKey": "Chucklefish:4373" + }, + + "Extended Fridge": { + "ID": "Crystalmir.ExtendedFridge", + "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 + "Default | UpdateKey": "Nexus:485", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Extended Greenhouse": { "ID": "ExtendedGreenhouse", - "UpdateKeys": [ "Chucklefish:4303" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Extended Minecart - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 - //"UpdateKeys": [ "Chucklefish:4359" ] // Entoarox opted out of mod update checks - }, - { - // Extended Reach + "Default | UpdateKey": "Chucklefish:4303", + "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Extended Minecart": { + "ID": "Entoarox.ExtendedMinecart", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'}", // changed in 1.6.1 + "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature) + }, + + "Extended Reach": { "ID": "spacechase0.ExtendedReach", - "UpdateKeys": [ "Nexus:1493" ] - }, - { - // Fall 28 Snow Day - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:486" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Automation: Barn Door Automation - "ID": "FarmAutomation.BarnDoorAutomation.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Automation: Item Collector - "ID": "FarmAutomation.ItemCollector.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Farm Automation Unofficial: Item Collector + "Default | UpdateKey": "Nexus:1493" + }, + + "Fall 28 Snow Day": { + "ID": "Omegasis.Fall28SnowDay", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:486", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Farm Automation: Barn Door Automation": { + "ID": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Farm Automation: Item Collector": { + "ID": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Farm Automation Unofficial: Item Collector": { "ID": "Maddy99.FarmAutomation.ItemCollector", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Expansion - "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5 | Advize.FarmExpansion", // changed in 2.0, 2.0.5, and 3.0 - "UpdateKeys": [ "Nexus:130" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Farm Resource Generator - "ID": "FarmResourceGenerator.dll", - "UpdateKeys": [ "Nexus:647" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Fast Animations + "~0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Farm Expansion": { + "ID": "Advize.FarmExpansion", + "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 + "Default | UpdateKey": "Nexus:130", + "~2.0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~2.0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Farm Resource Generator": { + "ID": "{EntryDll: 'FarmResourceGenerator.dll'}", + "Default | UpdateKey": "Nexus:647", + "~1.0.4 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.4 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Fast Animations": { "ID": "Pathoschild.FastAnimations", - "UpdateKeys": [ "Nexus:1089" ] - }, - { - // Faster Paths - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths" // changed in 1.2 and 1.3; disambiguate from Shop Expander - // "UpdateKeys": [ "Chucklefish:3641" ] // Entoarox opted out of mod update checks - }, - { - // Faster Run - "ID": "FasterRun.dll | KathrynHazuka.FasterRun", // changed in 1.1.1-pathoschild-update - "UpdateKeys": [ "Nexus:733" ], // added in 1.1.1-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Fishing Adjust + "Default | UpdateKey": "Nexus:1089" + }, + + "Faster Paths": { + "ID": "Entoarox.FasterPaths", + "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander + "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature) + }, + + "Faster Run": { + "ID": "KathrynHazuka.FasterRun", + "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update + "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update + "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Fishing Adjust": { "ID": "shuaiz.FishingAdjustMod", - "UpdateKeys": [ "Nexus:1350" ] + "Default | UpdateKey": "Nexus:1350" }, - { - // Fishing Tuner Redux + + "Fishing Tuner Redux": { "ID": "HammurabiFishingTunerRedux", - "UpdateKeys": [ "Chucklefish:4578" ] - }, - { - // FlorenceMod - "ID": "FlorenceMod.dll", - "UpdateKeys": [ "Nexus:591" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.0.1": "1.1" - } - }, - { - // Flower Color Picker + "Default | UpdateKey": "Chucklefish:4578" + }, + + "FlorenceMod": { + "ID": "{EntryDll: 'FlorenceMod.dll'}", + "MapLocalVersions": { "1.0.1": "1.1" }, + "Default | UpdateKey": "Nexus:591", + "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Flower Color Picker": { "ID": "spacechase0.FlowerColorPicker", - "UpdateKeys": [ "Nexus:1229" ] + "Default | UpdateKey": "Nexus:1229" }, - { - // Forage at the Farm + + "Forage at the Farm": { "ID": "ForageAtTheFarm", - "UpdateKeys": [ "Nexus:673" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Furniture Anywhere - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere" // changed in 1.1; disambiguate from Extended Minecart - // "UpdateKeys": [ "Chucklefish:4324" ] // Entoarox opted out of mod update checks - }, - { - // Game Reminder + "Default | UpdateKey": "Nexus:673", + "~1.5.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.5.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Furniture Anywhere": { + "ID": "Entoarox.FurnitureAnywhere", + "FormerIDs": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'}", // changed in 1.1; disambiguate from Extended Minecart + "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature) + }, + + "Game Reminder": { "ID": "mmanlapat.GameReminder", - "UpdateKeys": [ "Nexus:1153" ] - }, - { - // Gate Opener - "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 - "UpdateKeys": [ "GitHub:mralbobo/stardew-gate-opener" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // GenericShopExtender + "Default | UpdateKey": "Nexus:1153" + }, + + "Gate Opener": { + "ID": "mralbobo.GateOpener", + "FormerIDs": "{EntryDll: 'GateOpener.dll'}", // changed in 1.1 + "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "GenericShopExtender": { "ID": "GenericShopExtender", - "UpdateKeys": [ "Nexus:814" ], // added in 0.1.3 - "Compatibility": { - "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:814", // added in 0.1.3 + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Geode Info Menu + + "Geode Info Menu": { "ID": "cat.geodeinfomenu", - "UpdateKeys": [ "Nexus:1448" ] - }, - { - // Get Dressed - "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 - "UpdateKeys": [ "Nexus:331" ], - "Compatibility": { - "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Giant Crop Ring + "Default | UpdateKey": "Nexus:1448" + }, + + "Get Dressed": { + "ID": "Advize.GetDressed", + "FormerIDs": "{EntryDll: 'GetDressed.dll'}", // changed in 3.3 + "Default | UpdateKey": "Nexus:331", + "~3.3 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Giant Crop Ring": { "ID": "cat.giantcropring", - "UpdateKeys": [ "Nexus:1182" ] - }, - { - // Gift Taste Helper - "ID": "8008db57-fa67-4730-978e-34b37ef191d6 | tstaples.GiftTasteHelper", // changed in 2.5 - "UpdateKeys": [ "Nexus:229" ], - "Compatibility": { - "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Grandfather's Gift + "Default | UpdateKey": "Nexus:1182" + }, + + "Gift Taste Helper": { + "ID": "tstaples.GiftTasteHelper", + "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5 + "Default | UpdateKey": "Nexus:229", + "~2.3.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Grandfather's Gift": { "ID": "ShadowDragon.GrandfathersGift", - "UpdateKeys": [ "Nexus:985" ] + "Default | UpdateKey": "Nexus:985" }, - { - // Happy Animals + + "Happy Animals": { "ID": "HappyAnimals", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Happy Birthday (Omegasis) - "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'} | Omegasis.HappyBirthday", // changed in 1.4; disambiguate from Oxyligen's fork - "UpdateKeys": [ "Nexus:520" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Happy Birthday (Oxyligen fork) + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Happy Birthday (Omegasis)": { + "ID": "Omegasis.HappyBirthday", + "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // changed in 1.4; disambiguate from Oxyligen's fork + "Default | UpdateKey": "Nexus:520", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Happy Birthday (Oxyligen fork)": { "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "UpdateKeys": [ "Nexus:1064" ] + "Default | UpdateKey": "Nexus:1064" }, - { - // Harp of Yoba Redux + + "Harp of Yoba Redux": { "ID": "Platonymous.HarpOfYobaRedux", - "UpdateKeys": [ "Nexus:914" ] // added in 2.0.3 + "Default | UpdateKey": "Nexus:914" // added in 2.0.3 }, - { - // Harvest Moon Witch Princess + + "Harvest Moon Witch Princess": { "ID": "Sasara.WitchPrincess", - "UpdateKeys": [ "Nexus:1157" ] + "Default | UpdateKey": "Nexus:1157" }, - { - // Harvest With Scythe + + "Harvest With Scythe": { "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "UpdateKeys": [ "Nexus:236" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Horse Whistle (icepuente) + "Default | UpdateKey": "Nexus:236", + "~1.0.6 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Horse Whistle (icepuente)": { "ID": "icepuente.HorseWhistle", - "UpdateKeys": [ "Nexus:1131" ] + "Default | UpdateKey": "Nexus:1131" }, - { - // Hunger (Yyeadude) + + "Hunger (Yyeadude)": { "ID": "HungerYyeadude", - "UpdateKeys": [ "Nexus:613" ] + "Default | UpdateKey": "Nexus:613" }, - { - // Hunger for Food (Tigerle) + + "Hunger for Food (Tigerle)": { "ID": "HungerForFoodByTigerle", - "UpdateKeys": [ "Nexus:810" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Hunger Mod (skn) + "Default | UpdateKey": "Nexus:810", + "~0.1.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~0.1.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Hunger Mod (skn)": { "ID": "skn.HungerMod", - "UpdateKeys": [ "Nexus:1127" ], - "MapRemoteVersions": { - "1.2.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1127" }, - { - // Idle Pause + + "Idle Pause": { "ID": "Veleek.IdlePause", - "UpdateKeys": [ "Nexus:1092" ], - "MapRemoteVersions": { - "1.2": "1.1" // manifest not updated - } + "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:1092" }, - { - // Improved Quality of Life + + "Improved Quality of Life": { "ID": "Demiacle.ImprovedQualityOfLife", - "UpdateKeys": [ "Nexus:1025" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:1025", + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Instant Geode + + "Instant Geode": { "ID": "InstantGeode", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Instant Grow Trees - "ID": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 - "UpdateKeys": [ "Nexus:173" ] - }, - { - // Interaction Helper + "~1.12 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Instant Grow Trees": { + "ID": "cantorsdust.InstantGrowTrees", + "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1 + "Default | UpdateKey": "Nexus:173" + }, + + "Interaction Helper": { "ID": "HammurabiInteractionHelper", - "UpdateKeys": [ "Chucklefish:4640" ], // added in 1.0.4-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Item Auto Stacker + "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Item Auto Stacker": { "ID": "cat.autostacker", - "UpdateKeys": [ "Nexus:1184" ], - "MapRemoteVersions": { - "1.0.1": "1.0" // manifest not updated - } - }, - { - // Jiggly Junimo Bundles - "ID": "JJB.dll | Greger.JigglyJunimoBundles", // changed in 1.1.2-pathoschild-update - "UpdateKeys": [ "GitHub:gr3ger/Stardew_JJB" ], // added in 1.0.4-pathoschild-update - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" - }, - { - // Junimo Farm + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1184" + }, + + "Jiggly Junimo Bundles": { + "ID": "Greger.JigglyJunimoBundles", + "FormerIDs": "{EntryDll: 'JJB.dll'}", // changed in 1.1.2-pathoschild-update + "Default | UpdateKey": "GitHub:gr3ger/Stardew_JJB" // added in 1.0.4-pathoschild-update + }, + + "Json Assets": { + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720" + }, + + "Junimo Farm": { "ID": "Platonymous.JunimoFarm", - "UpdateKeys": [ "Nexus:984" ], // added in 1.1.3 - "MapRemoteVersions": { - "1.1.2": "1.1.1" // manifest not updated - } + "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:984" // added in 1.1.3 }, - { - // Less Strict Over-Exertion (AntiExhaustion) + + "Less Strict Over-Exertion (AntiExhaustion)": { "ID": "BALANCEMOD_AntiExhaustion", - "UpdateKeys": [ "Nexus:637" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.0": "1.1" - } - }, - { - // Level Extender + "MapLocalVersions": { "0.0": "1.1" }, + "Default | UpdateKey": "Nexus:637", + "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Level Extender": { "ID": "Devin Lematty.Level Extender", - "UpdateKeys": [ "Nexus:1471" ], - "MapRemoteVersions": { - "1.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1471" }, - { - // Level Up Notifications + + "Level Up Notifications": { "ID": "Level Up Notifications", - "UpdateKeys": [ "Nexus:855" ] + "Default | UpdateKey": "Nexus:855" }, - { - // Location and Music Logging + + "Location and Music Logging": { "ID": "Brandy Lover.LMlog", - "UpdateKeys": [ "Nexus:1366" ] + "Default | UpdateKey": "Nexus:1366" }, - { - // Longevity + + "Longevity": { "ID": "RTGOAT.Longevity", - "UpdateKeys": [ "Nexus:649" ] - }, - { - // Lookup Anything - "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 - "UpdateKeys": [ "Nexus:541" ], - "Compatibility": { - "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Love Bubbles + "Default | UpdateKey": "Nexus:649" + }, + + "Lookup Anything": { + "ID": "Pathoschild.LookupAnything", + "FormerIDs": "LookupAnything", // changed in 1.10.1 + "Default | UpdateKey": "Nexus:541", + "~1.10.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Love Bubbles": { "ID": "LoveBubbles", - "UpdateKeys": [ "Nexus:1318" ] - }, - { - // Loved Labels - "ID": "LovedLabels.dll", - "UpdateKeys": [ "Nexus:279" ], - "Compatibility": { - "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Luck Skill - "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 - "UpdateKeys": [ "Nexus:521" ], - "Compatibility": { - "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Mail Framework + "Default | UpdateKey": "Nexus:1318" + }, + + "Loved Labels": { + "ID": "Advize.LovedLabels", + "FormerIDs": "{EntryDll: 'LovedLabels.dll'}", // changed in 2.1 + "Default | UpdateKey": "Nexus:279", + "~2.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Luck Skill": { + "ID": "spacechase0.LuckSkill", + "FormerIDs": "LuckSkill", // changed in 0.1.4 + "Default | UpdateKey": "Nexus:521", + "~0.1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Mail Framework": { "ID": "DIGUS.MailFrameworkMod", - "UpdateKeys": [ "Nexus:1536" ] - }, - { - // MailOrderPigs - "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:632" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Makeshift Multiplayer - "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 - "UpdateKeys": [ "Nexus:501" ], - "Compatibility": { - "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Map Image Exporter - "ID": "MapImageExporter | spacechase0.MapImageExporter", // changed in 1.0.2 - "UpdateKeys": [ "Nexus:1073" ] - }, - { - // Message Box [API]? (ChatMod) + "Default | UpdateKey": "Nexus:1536" + }, + + "MailOrderPigs": { + "ID": "jwdred.MailOrderPigs", + "FormerIDs": "{EntryDll: 'MailOrderPigs.dll'}", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:632", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Makeshift Multiplayer": { + "ID": "spacechase0.StardewValleyMP", + "FormerIDs": "StardewValleyMP", // changed in 0.3 + "Default | UpdateKey": "Nexus:501", + "~0.3.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Map Image Exporter": { + "ID": "spacechase0.MapImageExporter", + "FormerIDs": "MapImageExporter", // changed in 1.0.2 + "Default | UpdateKey": "Nexus:1073" + }, + + "Message Box [API]? (ChatMod)": { "ID": "Kithio:ChatMod", - "UpdateKeys": [ "Chucklefish:4296" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Mining at the Farm + "Default | UpdateKey": "Chucklefish:4296", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Mining at the Farm": { "ID": "MiningAtTheFarm", - "UpdateKeys": [ "Nexus:674" ] + "Default | UpdateKey": "Nexus:674" }, - { - // Mining With Explosives + + "Mining With Explosives": { "ID": "MiningWithExplosives", - "UpdateKeys": [ "Nexus:770" ] + "Default | UpdateKey": "Nexus:770" }, - { - // Modder Serialization Utility + + "Modder Serialization Utility": { "ID": "SerializerUtils-0-1", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." - } - } - }, - { - // More Artifact Spots + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "More Animals": { + "ID": "Entoarox.MoreAnimals", + "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 + "~2.0.2 | UpdateKey": "Chucklefish:4288", // only enable update checks up to 2.0.2 by request (has its own update-check feature) + "~1.3.2 | Status": "AssumeBroken" // overhauled for SMAPI 1.11+ compatibility + }, + + "More Artifact Spots": { "ID": "451", - "UpdateKeys": [ "Nexus:451" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // More Map Layers + "Default | UpdateKey": "Nexus:451", + "~1.0.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "More Map Layers": { "ID": "Platonymous.MoreMapLayers", - "UpdateKeys": [ "Nexus:1134" ] // added in 1.1.1 - }, - { - // More Pets - "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - // "UpdateKeys": [ "Chucklefish:4288" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility - } - }, - { - // More Rain - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:441" ], // added in 1.5.1 - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // More Weapons + "Default | UpdateKey": "Nexus:1134" // added in 1.1.1 + }, + + "More Rain": { + "ID": "Omegasis.MoreRain", + "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:441", // added in 1.5.1 + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "More Weapons": { "ID": "Joco80.MoreWeapons", - "UpdateKeys": [ "Nexus:1168" ] + "Default | UpdateKey": "Nexus:1168" }, - { - // Move Faster + + "Move Faster": { "ID": "shuaiz.MoveFasterMod", - "UpdateKeys": [ "Nexus:1351" ] + "Default | UpdateKey": "Nexus:1351" }, - { - // Multiple Sprites and Portraits On Rotation (File Loading) + + "Multiple Sprites and Portraits On Rotation (File Loading)": { "ID": "FileLoading", - "UpdateKeys": [ "Nexus:1094" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.1": "1.12" - } - }, - { - // Museum Rearranger - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'} | Omegasis.MuseumRearranger", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:428" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // New Machines + "MapLocalVersions": { "1.1": "1.12" }, + "Default | UpdateKey": "Nexus:1094", + "~1.12 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Museum Rearranger": { + "ID": "Omegasis.MuseumRearranger", + "FormerIDs": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:428", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "New Machines": { "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "UpdateKeys": [ "Chucklefish:3683" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Night Owl - "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere - "UpdateKeys": [ "Nexus:433" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "2.1": "1.3" // 1.3 had wrong version in manifest - } - }, - { - // No Kids Ever + "Default | UpdateKey": "Chucklefish:3683", + "~4.2.1343 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~4.2.1343 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Night Owl": { + "ID": "Omegasis.NightOwl", + "FormerIDs": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // changed in 1.4; disambiguate from Save Anywhere + "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest + "Default | UpdateKey": "Nexus:433", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "No Kids Ever": { "ID": "Hangy.NoKidsEver", - "UpdateKeys": [ "Nexus:1464" ] + "Default | UpdateKey": "Nexus:1464" }, - { - // No Debug Mode + + "No Debug Mode": { "ID": "NoDebugMode", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "debug mode was removed in SMAPI 1.0." - } - } - }, - { - // No Fence Decay + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, + + "No Fence Decay": { "ID": "cat.nofencedecay", - "UpdateKeys": [ "Nexus:1180" ] + "Default | UpdateKey": "Nexus:1180" }, - { - // No More Pets - "ID": "NoMorePets | Omegasis.NoMorePets", // changed in 1.4 - "UpdateKeys": [ "Nexus:506" ] // added in 1.4.1 + + "No More Pets": { + "ID": "Omegasis.NoMorePets", + "FormerIDs": "NoMorePets", // changed in 1.4 + "Default | UpdateKey": "Nexus:506" // added in 1.4.1 }, - { - // NoSoilDecay + + "NoSoilDecay": { "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "UpdateKeys": [ "Nexus:237" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location - } - }, - { - // No Soil Decay Redux + "Default | UpdateKey": "Nexus:237", + "~0.5 | Status": "AssumeBroken", // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location + "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "No Soil Decay Redux": { "ID": "Platonymous.NoSoilDecayRedux", - "UpdateKeys": [ "Nexus:1084" ] // added in 1.1.9 + "Default | UpdateKey": "Nexus:1084" // added in 1.1.9 }, - { - // NPC Map Locations + + "NPC Map Locations": { "ID": "NPCMapLocationsMod", - "UpdateKeys": [ "Nexus:239" ], - "Compatibility": { - "1.42~1.43": { - "Status": "AssumeBroken", - "ReasonPhrase": "this version has an update check error which crashes the game." - } - } - }, - { - // NPC Speak - "ID": "NpcEcho.dll", - "UpdateKeys": [ "Nexus:694" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Object Time Left + "Default | UpdateKey": "Nexus:239", + "1.42~1.43 | Status": "AssumeBroken", + "1.42~1.43 | StatusReasonPhrase": "this version has an update check error which crashes the game." + }, + + "NPC Speak": { + "ID": "{EntryDll: 'NpcEcho.dll'}", + "Default | UpdateKey": "Nexus:694", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Object Time Left": { "ID": "spacechase0.ObjectTimeLeft", - "UpdateKeys": [ "Nexus:1315" ] - }, - { - // OmniFarm - "ID": "BlueMod_OmniFarm | PhthaloBlue.OmniFarm", // changed in 2.0.2-pathoschild-update - "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_OmniFarm" ], - "Compatibility": { - "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Out of Season Bonuses / Seasonal Items + "Default | UpdateKey": "Nexus:1315" + }, + + "OmniFarm": { + "ID": "PhthaloBlue.OmniFarm", + "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm", + "~2.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Out of Season Bonuses (Seasonal Items)": { "ID": "midoriarmstrong.seasonalitems", - "UpdateKeys": [ "Nexus:1452" ] + "Default | UpdateKey": "Nexus:1452" }, - { - // Part of the Community + + "Part of the Community": { "ID": "SB_PotC", - "UpdateKeys": [ "Nexus:923" ], - "Compatibility": { - "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // PelicanFiber - "ID": "PelicanFiber.dll | jwdred.PelicanFiber", // changed in 3.0.1 - "UpdateKeys": [ "Nexus:631" ], - "Compatibility": { - "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "3.0.2": "3.0.1" // didn't change manifest version - } - }, - { - // PelicanTTS + "Default | UpdateKey": "Nexus:923", + "~1.0.8 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "PelicanFiber": { + "ID": "jwdred.PelicanFiber", + "FormerIDs": "{EntryDll: 'PelicanFiber.dll'}", // changed in 3.0.1 + "MapRemoteVersions": { "3.0.2": "3.0.1" }, // didn't change manifest version + "Default | UpdateKey": "Nexus:631", + "~3.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "PelicanTTS": { "ID": "Platonymous.PelicanTTS", - "UpdateKeys": [ "Nexus:1079" ], // added in 1.6.1 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Persia the Mermaid - Standalone Custom NPC + "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 + "~1.6 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Persia the Mermaid - Standalone Custom NPC": { "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", - "UpdateKeys": [ "Nexus:1419" ] - }, - { - // Persival's BundleMod - "ID": "BundleMod.dll", - "UpdateKeys": [ "Nexus:438" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 - } - }, - { - // Plant on Grass + "Default | UpdateKey": "Nexus:1419" + }, + + "Persival's BundleMod": { + "ID": "{EntryDll: 'BundleMod.dll'}", + "Default | UpdateKey": "Nexus:438", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Plant on Grass": { "ID": "Demiacle.PlantOnGrass", - "UpdateKeys": [ "Nexus:1026" ] - }, - { - // Point-and-Plant - "ID": "PointAndPlant.dll | jwdred.PointAndPlant", // changed in 1.0.3 - "UpdateKeys": [ "Nexus:572" ], - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Pony Weight Loss Program + "Default | UpdateKey": "Nexus:1026" + }, + + "PyTK - Platonymous Toolkit": { + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" + }, + + "Point-and-Plant": { + "ID": "jwdred.PointAndPlant", + "FormerIDs": "{EntryDll: 'PointAndPlant.dll'}", // changed in 1.0.3 + "Default | UpdateKey": "Nexus:572", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Pony Weight Loss Program": { "ID": "BadNetCode.PonyWeightLossProgram", - "UpdateKeys": [ "Nexus:1232" ] + "Default | UpdateKey": "Nexus:1232" }, - { - // Portraiture + + "Portraiture": { "ID": "Platonymous.Portraiture", - "UpdateKeys": [ "Nexus:999" ] // added in 1.3.1 - }, - { - // Prairie King Made Easy - "ID": "PrairieKingMadeEasy.dll | Mucchan.PrairieKingMadeEasy", // changed in 1.0.1 - "UpdateKeys": [ "Chucklefish:3594" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Quest Delay + "Default | UpdateKey": "Nexus:999" // added in 1.3.1 + }, + + "Prairie King Made Easy": { + "ID": "Mucchan.PrairieKingMadeEasy", + "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 + "Default | UpdateKey": "Chucklefish:3594", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Quest Delay": { "ID": "BadNetCode.QuestDelay", - "UpdateKeys": [ "Nexus:1239" ] - }, - { - // Rain Randomizer - "ID": "RainRandomizer.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Recatch Legendary Fish - "ID": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 - "UpdateKeys": [ "Nexus:172" ] - }, - { - // Regeneration + "Default | UpdateKey": "Nexus:1239" + }, + + "Rain Randomizer": { + "ID": "{EntryDll: 'RainRandomizer.dll'}", + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Recatch Legendary Fish": { + "ID": "cantorsdust.RecatchLegendaryFish", + "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1 + "Default | UpdateKey": "Nexus:172" + }, + + "Regeneration": { "ID": "HammurabiRegeneration", - "UpdateKeys": [ "Chucklefish:4584" ] + "Default | UpdateKey": "Chucklefish:4584" }, - { - // Relationship Bar UI + + "Relationship Bar UI": { "ID": "RelationshipBar", - "UpdateKeys": [ "Nexus:1009" ] + "Default | UpdateKey": "Nexus:1009" }, - { - // RelationshipsEnhanced + + "RelationshipsEnhanced": { "ID": "relationshipsenhanced", - "UpdateKeys": [ "Chucklefish:4435" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Relationship Status + "Default | UpdateKey": "Chucklefish:4435", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Relationship Status": { "ID": "relationshipstatus", - "UpdateKeys": [ "Nexus:751" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.0.5": "1.0.4" // not updated in manifest - } - }, - { - // Rented Tools + "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest + "Default | UpdateKey": "Nexus:751", + "~1.0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Rented Tools": { "ID": "JarvieK.RentedTools", - "UpdateKeys": [ "Nexus:1307" ] - }, - { - // Replanter - "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 - "UpdateKeys": [ "Nexus:589" ], - "Compatibility": { - "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // ReRegeneration + "Default | UpdateKey": "Nexus:1307" + }, + + "Replanter": { + "ID": "jwdred.Replanter", + "FormerIDs": "{EntryDll: 'Replanter.dll'}", // changed in 1.0.5 + "Default | UpdateKey": "Nexus:589", + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "ReRegeneration": { "ID": "lrsk_sdvm_rerg.0925160827", - "UpdateKeys": [ "Chucklefish:4465" ], - "MapLocalVersions": { - "1.1.2-release": "1.1.2" - } + "MapLocalVersions": { "1.1.2-release": "1.1.2" }, + "Default | UpdateKey": "Chucklefish:4465" }, - { - // Reseed + + "Reseed": { "ID": "Roc.Reseed", - "UpdateKeys": [ "Nexus:887" ] + "Default | UpdateKey": "Nexus:887" }, - { - // Reusable Wallpapers and Floors (Wallpaper Retain) + + "Reusable Wallpapers and Floors (Wallpaper Retain)": { "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "UpdateKeys": [ "Nexus:356" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Ring of Fire + "Default | UpdateKey": "Nexus:356", + "~1.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Ring of Fire": { "ID": "Platonymous.RingOfFire", - "UpdateKeys": [ "Nexus:1166" ] // added in 1.0.1 + "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 }, - { - // Rope Bridge + + "Rope Bridge": { "ID": "RopeBridge", - "UpdateKeys": [ "Nexus:824" ] + "Default | UpdateKey": "Nexus:824" }, - { - // Rotate Toolbar + + "Rotate Toolbar": { "ID": "Pathoschild.RotateToolbar", - "UpdateKeys": [ "Nexus:1100" ] - }, - { - // Rush Orders - "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 - "UpdateKeys": [ "Nexus:605" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Save Anywhere - "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl - "UpdateKeys": [ "Nexus:444" ], // added in 2.6.1 - "Compatibility": { - "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Save Backup - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'} | Omegasis.SaveBackup", // changed in 1.3; disambiguate from other Alpha_Omegasis mods - "UpdateKeys": [ "Nexus:435" ], // added in 1.3.1 - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Scroll to Blank + "Default | UpdateKey": "Nexus:1100" + }, + + "Rush Orders": { + "ID": "spacechase0.RushOrders", + "FormerIDs": "RushOrders", // changed in 1.1 + "Default | UpdateKey": "Nexus:605", + "~1.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Save Anywhere": { + "ID": "Omegasis.SaveAnywhere", + "FormerIDs": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // changed in 2.5; disambiguate from Night Owl + "Default | UpdateKey": "Nexus:444", // added in 2.6.1 + "~2.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Save Backup": { + "ID": "Omegasis.SaveBackup", + "FormerIDs": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // changed in 1.3; disambiguate from other Alpha_Omegasis mods + "Default | UpdateKey": "Nexus:435", // added in 1.3.1 + "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Scroll to Blank": { "ID": "caraxian.scroll.to.blank", - "UpdateKeys": [ "Chucklefish:4405" ] - }, - { - // Scythe Harvesting - "ID": "ScytheHarvesting | mmanlapat.ScytheHarvesting", // changed in 1.6 - "UpdateKeys": [ "Nexus:1106" ] - }, - { - // Seasonal Immersion - "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - // "UpdateKeys": [ "Chucklefish:4262" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Seed Bag + "Default | UpdateKey": "Chucklefish:4405" + }, + + "Scythe Harvesting": { + "ID": "mmanlapat.ScytheHarvesting", + "FormerIDs": "ScytheHarvesting", // changed in 1.6 + "Default | UpdateKey": "Nexus:1106" + }, + + "Seasonal Immersion": { + "ID": "Entoarox.SeasonalImmersion", + "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 + "~1.11 | UpdateKey": "Chucklefish:4262", // only enable update checks up to 1.11 by request (has its own update-check feature) + "~1.8.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Seed Bag": { "ID": "Platonymous.SeedBag", - "UpdateKeys": [ "Nexus:1133" ] // added in 1.1.2 + "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 }, - { - // Self Service + + "Self Service": { "ID": "JarvieK.SelfService", - "UpdateKeys": [ "Nexus:1304" ], - "MapRemoteVersions": { - "0.2.1": "0.2" // manifest not updated - } + "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated + "Default | UpdateKey": "Nexus:1304" }, - { - // Send Items + + "Send Items": { "ID": "Denifia.SendItems", - "UpdateKeys": [ "Nexus:1087" ], // added in 1.0.3 (2017-10-04) - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "Default | UpdateKey": "Nexus:1087", // added in 1.0.3 (2017-10-04) + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // Shed Notifications (BuildingsNotifications) + + "Shed Notifications (BuildingsNotifications)": { "ID": "TheCroak.BuildingsNotifications", - "UpdateKeys": [ "Nexus:620" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Shenandoah Project + "Default | UpdateKey": "Nexus:620", + "~0.4.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~0.4.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Shenandoah Project": { "ID": "Shenandoah Project", - "UpdateKeys": [ "Nexus:756" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.1.1": "1.1" // not updated in manifest - } - }, - { - // Ship Anywhere + "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest + "Default | UpdateKey": "Nexus:756", + "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Ship Anywhere": { "ID": "spacechase0.ShipAnywhere", - "UpdateKeys": [ "Nexus:1379" ] + "Default | UpdateKey": "Nexus:1379" }, - { - // Shipment Tracker + + "Shipment Tracker": { "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "UpdateKeys": [ "Nexus:321" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Shop Expander - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - // "UpdateKeys": [ "Chucklefish:4381" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Showcase Mod + "Default | UpdateKey": "Nexus:321", + "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Shop Expander": { + "ID": "Entoarox.ShopExpander", + "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths + "~1.5.3 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.5.3 by request (has its own update-check feature) + "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Showcase Mod": { "ID": "Igorious.Showcase", - "UpdateKeys": [ "Chucklefish:4487" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.9-500": "0.9" - } - }, - { - // Shroom Spotter + "MapLocalVersions": { "0.9-500": "0.9" }, + "Default | UpdateKey": "Chucklefish:4487", + "~0.9 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~0.9 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Shroom Spotter": { "ID": "TehPers.ShroomSpotter", - "UpdateKeys": [ "Nexus:908" ] + "Default | UpdateKey": "Nexus:908" }, - { - // Simple Crop Label + + "Simple Crop Label": { "ID": "SimpleCropLabel", - "UpdateKeys": [ "Nexus:314" ] + "Default | UpdateKey": "Nexus:314" }, - { - // Simple Sound Manager + + "Simple Sound Manager": { "ID": "Omegasis.SimpleSoundManager", - "UpdateKeys": [ "Nexus:1410" ], // added in 1.0.1 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", // can remove once 1.0.1 is published - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Simple Sprinklers - "ID": "SimpleSprinkler.dll | tZed.SimpleSprinkler", // changed in 1.5 - "UpdateKeys": [ "Nexus:76" ], - "Compatibility": { - "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Siv's Marriage Mod + "Default | UpdateKey": "Nexus:1410", // added in 1.0.1 + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Simple Sprinklers": { + "ID": "tZed.SimpleSprinkler", + "FormerIDs": "{EntryDll: 'SimpleSprinkler.dll'}", // changed in 1.5 + "Default | UpdateKey": "Nexus:76", + "~1.4 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Siv's Marriage Mod": { "ID": "6266959802", - "UpdateKeys": [ "Nexus:366" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) - }, - "MapLocalVersions": { - "0.0": "1.4" - } - }, - { - // Skill Prestige - "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b | alphablackwolf.skillPrestige", // changed circa 1.2.3 - "UpdateKeys": [ "Nexus:569" ], - "Compatibility": { - "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Skill Prestige: Cooking Adapter - "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63 | Alphablackwolf.CookingSkillPrestigeAdapter", // changed circa 1.1 - "UpdateKeys": [ "Nexus:569" ], - "Compatibility": { - "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapRemoteVersions": { - "1.2.3": "1.1" // manifest not updated - } - }, - { - // Skip Intro - "ID": "SkipIntro | Pathoschild.SkipIntro", // changed in 1.4 - "UpdateKeys": [ "Nexus:533" ] - }, - { - // Skull Cavern Elevator + "MapLocalVersions": { "0.0": "1.4" }, + "Default | UpdateKey": "Nexus:366", + "~1.2.2 | Status": "AssumeBroken", // broke in SMAPI 1.9 (has multiple Mod instances) + "~1.2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Skill Prestige": { + "ID": "alphablackwolf.skillPrestige", + "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3 + "Default | UpdateKey": "Nexus:569", + "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Skill Prestige: Cooking Adapter": { + "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", + "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 + "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated + "Default | UpdateKey": "Nexus:569", + "~1.0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "FormerIDs": "SkipIntro", // changed in 1.4 + "Default | UpdateKey": "Nexus:533" + }, + + "Skull Cavern Elevator": { "ID": "SkullCavernElevator", - "UpdateKeys": [ "Nexus:963" ] + "Default | UpdateKey": "Nexus:963" }, - { - // Skull Cave Saver - "ID": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 - "UpdateKeys": [ "Nexus:175" ] + + "Skull Cave Saver": { + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 + "Default | UpdateKey": "Nexus:175" }, - { - // Sleepy Eye + + "Sleepy Eye": { "ID": "spacechase0.SleepyEye", - "UpdateKeys": [ "Nexus:1152" ] - }, - { - // Slower Fence Decay - "ID": "SPDSlowFenceDecay | Speeder.SlowerFenceDecay", // changed in 0.5.2-pathoschild-update - "UpdateKeys": [ "Nexus:252" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Smart Mod + "Default | UpdateKey": "Nexus:1152" + }, + + "Slower Fence Decay": { + "ID": "Speeder.SlowerFenceDecay", + "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update + "Default | UpdateKey": "Nexus:252", + "~0.5.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~0.5.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Smart Mod": { "ID": "KuroBear.SmartMod", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~2.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" }, - { - // Solar Eclipse Event + + "Solar Eclipse Event": { "ID": "KoihimeNakamura.SolarEclipseEvent", - "UpdateKeys": [ "Nexus:897" ], - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "1.3-20170917": "1.3" - } - }, - { - // SpaceCore + "Default | UpdateKey": "Nexus:897", + "MapLocalVersions": { "1.3-20170917": "1.3" }, + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "SpaceCore": { "ID": "spacechase0.SpaceCore", - "UpdateKeys": [ "Nexus:1348" ] + "Default | UpdateKey": "Nexus:1348" }, - { - // Speedster + + "Speedster": { "ID": "Platonymous.Speedster", - "UpdateKeys": [ "Nexus:1102" ] // added in 1.3.1 + "Default | UpdateKey": "Nexus:1102" // added in 1.3.1 }, - { - // Sprinkler Range + + "Sprinkler Range": { "ID": "cat.sprinklerrange", - "UpdateKeys": [ "Nexus:1179" ], - "MapRemoteVersions": { - "1.0.1": "1.0" // manifest not updated - } + "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated + "Default | UpdateKey": "Nexus:1179" }, - { - // Sprinkles + + "Sprinkles": { "ID": "Platonymous.Sprinkles", - "UpdateKeys": [ "Chucklefish:4592" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Sprint and Dash + "Default | UpdateKey": "Chucklefish:4592", + "~1.1.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Sprint and Dash": { "ID": "SPDSprintAndDash", - "UpdateKeys": [ "Chucklefish:3531" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Sprint and Dash Redux - "ID": "lrsk_sdvm_sndr.0921161059 | littleraskol.SprintAndDashRedux", // changed in 1.3 - "UpdateKeys": [ "Chucklefish:4201" ] - }, - { - // Sprinting Mod + "Default | UpdateKey": "Chucklefish:3531", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Sprint and Dash Redux": { + "ID": "littleraskol.SprintAndDashRedux", + "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3 + "Default | UpdateKey": "Chucklefish:4201" + }, + + "Sprinting Mod": { "ID": "a10d3097-b073-4185-98ba-76b586cba00c", - "UpdateKeys": [ "GitHub:oliverpl/SprintingMod" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapLocalVersions": { - "1.0": "2.1" // not updated in manifest - } - }, - { - // StackSplitX - "ID": "StackSplitX.dll | tstaples.StackSplitX", // changed circa 1.3.1 - "UpdateKeys": [ "Nexus:798" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // StaminaRegen - "ID": "StaminaRegen.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Stardew Config Menu + "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest + "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", + "~2.1 | Status": "AssumeBroken", // broke in SDV 1.2 + "~2.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "StackSplitX": { + "ID": "tstaples.StackSplitX", + "FormerIDs": "{EntryDll: 'StackSplitX.dll'}", // changed circa 1.3.1 + "Default | UpdateKey": "Nexus:798", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "StaminaRegen": { + "ID": "{EntryDll: 'StaminaRegen.dll'}", + "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Stardew Config Menu": { "ID": "Juice805.StardewConfigMenu", - "UpdateKeys": [ "Nexus:1312" ] + "Default | UpdateKey": "Nexus:1312" }, - { - // Stardew Content Compatibility Layer (SCCL) + + "Stardew Content Compatibility Layer (SCCL)": { "ID": "SCCL", - "UpdateKeys": [ "Nexus:889" ], - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "Default | UpdateKey": "Nexus:889", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, - { - // Stardew Editor Game Integration + + "Stardew Editor Game Integration": { "ID": "spacechase0.StardewEditor.GameIntegration", - "UpdateKeys": [ "Nexus:1298" ] + "Default | UpdateKey": "Nexus:1298" }, - { - // Stardew Notification + + "Stardew Notification": { "ID": "stardewnotification", - "UpdateKeys": [ "GitHub:monopandora/StardewNotification" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Stardew Symphony - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'} | Omegasis.StardewSymphony", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:425" ], // added in 1.4.1 - "Compatibility": { - "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // StarDustCore + "Default | UpdateKey": "GitHub:monopandora/StardewNotification", + "~1.7 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.7 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Stardew Symphony": { + "ID": "Omegasis.StardewSymphony", + "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // changed in 1.4; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:425", // added in 1.4.1 + "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "StarDustCore": { "ID": "StarDustCore", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - } - } - }, - { - // Starting Money - "ID": "StartingMoney | mmanlapat.StartingMoney", // changed in 1.1 - "UpdateKeys": [ "Nexus:1138" ] - }, - { - // StashItemsToChest + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, + + "Starting Money": { + "ID": "mmanlapat.StartingMoney", + "FormerIDs": "StartingMoney", // changed in 1.1 + "Default | UpdateKey": "Nexus:1138" + }, + + "StashItemsToChest": { "ID": "BlueMod_StashItemsToChest", - "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_StashItemsToChest" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Stephan's Lots of Crops + "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", + "~1.0.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Stephan's Lots of Crops": { "ID": "stephansstardewcrops", - "UpdateKeys": [ "Chucklefish:4314" ], - "MapRemoteVersions": { - "1.41": "1.1" // manifest not updated - } - }, - { - // Stone Bridge Over Pond (PondWithBridge) - "ID": "PondWithBridge.dll", - "UpdateKeys": [ "Nexus:316" ], - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - }, - "MapLocalVersions": { - "0.0": "1.0" - } - }, - { - // Stumps to Hardwood Stumps + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "Default | UpdateKey": "Chucklefish:4314" + }, + + "Stone Bridge Over Pond (PondWithBridge)": { + "ID": "{EntryDll: 'PondWithBridge.dll'}", + "MapLocalVersions": { "0.0": "1.0" }, + "Default | UpdateKey": "Nexus:316", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + + "Stumps to Hardwood Stumps": { "ID": "StumpsToHardwoodStumps", - "UpdateKeys": [ "Nexus:691" ] + "Default | UpdateKey": "Nexus:691" }, - { - // Super Greenhouse Warp Modifier + + "Super Greenhouse Warp Modifier": { "ID": "SuperGreenhouse", - "UpdateKeys": [ "Chucklefish:4334" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Swim Almost Anywhere / Swim Suit + "Default | UpdateKey": "Chucklefish:4334", + "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Swim Almost Anywhere / Swim Suit": { "ID": "Platonymous.SwimSuit", - "UpdateKeys": [ "Nexus:1215" ] // added in 0.5.1 - }, - { - // Tainted Cellar - "ID": "TaintedCellar.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 - } - }, - { - // Tapper Ready + "Default | UpdateKey": "Nexus:1215" // added in 0.5.1 + }, + + "Tainted Cellar": { + "ID": "{EntryDll: 'TaintedCellar.dll'}", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Tapper Ready": { "ID": "skunkkk.TapperReady", - "UpdateKeys": [ "Nexus:1219" ] + "Default | UpdateKey": "Nexus:1219" }, - { - // Teh's Fishing Overhaul + + "Teh's Fishing Overhaul": { "ID": "TehPers.FishingOverhaul", - "UpdateKeys": [ "Nexus:866" ] + "Default | UpdateKey": "Nexus:866" }, - { - // Teleporter + + "Teleporter": { "ID": "Teleporter", - "UpdateKeys": [ "Chucklefish:4374" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // The Long Night + "Default | UpdateKey": "Chucklefish:4374", + "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "The Long Night": { "ID": "Pathoschild.TheLongNight", - "UpdateKeys": [ "Nexus:1369" ] + "Default | UpdateKey": "Nexus:1369" }, - { - // Three-heart Dance Partner + + "Three-heart Dance Partner": { "ID": "ThreeHeartDancePartner", - "UpdateKeys": [ "Nexus:500" ], - "Compatibility": { - "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // TimeFreeze - "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe | Omegasis.TimeFreeze", // changed in 1.2 - "UpdateKeys": [ "Nexus:973" ] // added in 1.2.1 - }, - { - // Time Reminder + "Default | UpdateKey": "Nexus:500", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "TimeFreeze": { + "ID": "Omegasis.TimeFreeze", + "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2 + "Default | UpdateKey": "Nexus:973" // added in 1.2.1 + }, + + "Time Reminder": { "ID": "KoihimeNakamura.TimeReminder", - "UpdateKeys": [ "Nexus:1000" ], - "MapLocalVersions": { - "1.0-20170314": "1.0.2" - } - }, - { - // TimeSpeed - "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis - "UpdateKeys": [ "Nexus:169" ], - "Compatibility": { - "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // TractorMod - "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "UpdateKeys": [ "Nexus:1401" ] - }, - { - // TrainerMod + "MapLocalVersions": { "1.0-20170314": "1.0.2" }, + "Default | UpdateKey": "Nexus:1000" + }, + + "TimeSpeed": { + "ID": "cantorsdust.TimeSpeed", + "FormerIDs": "{EntryDll: 'TimeSpeed.dll'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3, 2.1, and 2.3.3; disambiguate other mods by Alpha_Omegasis + "Default | UpdateKey": "Nexus:169", + "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "TractorMod": { + "ID": "Pathoschild.TractorMod", + "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "Default | UpdateKey": "Nexus:1401" + }, + + "TrainerMod": { "ID": "SMAPI.TrainerMod", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." - } - } - }, - { - // Tree Transplant + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer." + }, + + "Tree Transplant": { "ID": "TreeTransplant", - "UpdateKeys": [ "Nexus:1342" ] + "Default | UpdateKey": "Nexus:1342" }, - { - // UI Info Suite + + "UI Info Suite": { "ID": "Cdaragorn.UiInfoSuite", - "UpdateKeys": [ "Nexus:1150" ] + "Default | UpdateKey": "Nexus:1150" }, - { - // UiModSuite + + "UiModSuite": { "ID": "Demiacle.UiModSuite", - "UpdateKeys": [ "Nexus:1023" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapLocalVersions": { - "0.5": "1.0" // not updated in manifest - } - }, - { - // Variable Grass + "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest + "Default | UpdateKey": "Nexus:1023", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Variable Grass": { "ID": "dantheman999.VariableGrass", - "UpdateKeys": [ "GitHub:dantheman999301/StardewMods" ] + "Default | UpdateKey": "GitHub:dantheman999301/StardewMods" }, - { - // Vertical Toolbar + + "Vertical Toolbar": { "ID": "SB_VerticalToolMenu", - "UpdateKeys": [ "Nexus:943" ] - }, - { - // WakeUp - "ID": "WakeUp.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // Wallpaper Fix - "ID": "WallpaperFix.dll", - "UpdateKeys": [ "Chucklefish:4211" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } - }, - { - // WarpAnimals + "Default | UpdateKey": "Nexus:943" + }, + + "WakeUp": { + "ID": "{EntryDll: 'WakeUp.dll'}", + "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "Wallpaper Fix": { + "ID": "{EntryDll: 'WallpaperFix.dll'}", + "Default | UpdateKey": "Chucklefish:4211", + "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 + "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "WarpAnimals": { "ID": "Symen.WarpAnimals", - "UpdateKeys": [ "Nexus:1400" ] - }, - { - // Weather Controller - "ID": "WeatherController.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // What Farm Cave / WhatAMush + "Default | UpdateKey": "Nexus:1400" + }, + + "Weather Controller": { + "ID": "{EntryDll: 'WeatherController.dll'}", + "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 + "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "What Farm Cave / WhatAMush": { "ID": "WhatAMush", - "UpdateKeys": [ "Nexus:1097" ] + "Default | UpdateKey": "Nexus:1097" }, - { - // WHats Up + + "WHats Up": { "ID": "wHatsUp", - "UpdateKeys": [ "Nexus:1082" ] - }, - { - // Wonderful Farm Life - "ID": "WonderfulFarmLife.dll", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 - } - }, - { - // XmlSerializerRetool - "ID": "XmlSerializerRetool.dll", - "Compatibility": { - "~": { - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." - } - } - }, - { - // Xnb Loader + "Default | UpdateKey": "Nexus:1082" + }, + + "Wonderful Farm Life": { + "ID": "{EntryDll: 'WonderfulFarmLife.dll'}", + "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 + "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + }, + + "XmlSerializerRetool": { + "ID": "{EntryDll: 'XmlSerializerRetool.dll'}", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "Xnb Loader": { "ID": "Entoarox.XnbLoader", - // "UpdateKeys": [ "Chucklefish:4506" ], // Entoarox opted out of mod update checks - "Compatibility": { - "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 - } + "~1.1.10 | UpdateKey": "Chucklefish:4506", // only enable update checks up to 1.1.10 by request (has its own update-check feature) + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - { - // zDailyIncrease + + "zDailyIncrease": { "ID": "zdailyincrease", - "UpdateKeys": [ "Chucklefish:4247" ], - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - }, - "MapRemoteVersions": { - "1.3.5": "1.3.4" // not updated in manifest - } - }, - { - // Zoom Out Extreme - "ID": "ZoomMod | RockinMods.ZoomMod", // changed circa 1.2.1 - "UpdateKeys": [ "Nexus:1326" ], - "Compatibility": { - "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Better RNG - "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Calendar Anywhere - "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Durable Fences - "ID": "56d3439c-7b9b-497e-9496-0c4890e8a00e | Zoryn.DurableFences", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] - }, - { - // Zoryn's Health Bars - "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Fishing Mod - "ID": "fa277b1f-265e-47c3-a84f-cd320cc74949 | Zoryn.FishingMod", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] - }, - { - // Zoryn's Junimo Deposit Anywhere - "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Movement Mod - "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } - }, - { - // Zoryn's Regen Mod - "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 - "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], - "Compatibility": { - "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest + "Default | UpdateKey": "Chucklefish:4247", + "~1.2 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoom Out Extreme": { + "ID": "RockinMods.ZoomMod", + "FormerIDs": "ZoomMod", // changed circa 1.2.1 + "Default | UpdateKey": "Nexus:1326", + "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Better RNG": { + "ID": "Zoryn.BetterRNG", + "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Calendar Anywhere": { + "ID": "Zoryn.CalendarAnywhere", + "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Durable Fences": { + "ID": "Zoryn.DurableFences", + "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Health Bars": { + "ID": "Zoryn.HealthBars", + "FormerIDs": "{EntryDll: 'HealthBars.dll'}", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Fishing Mod": { + "ID": "Zoryn.FishingMod", + "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods" + }, + + "Zoryn's Junimo Deposit Anywhere": { + "ID": "Zoryn.JunimoDepositAnywhere", + "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Movement Mod": { + "ID": "Zoryn.MovementModifier", + "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Zoryn's Regen Mod": { + "ID": "Zoryn.RegenMod", + "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6 + "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods", + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2 } - ] + } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index cd6c4ac3..8ef3022f 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -85,11 +85,20 @@ <Compile Include="..\..\build\GlobalAssemblyInfo.cs"> <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> + <Compile Include="Framework\Events\EventManager.cs" /> + <Compile Include="Framework\Events\ManagedEvent.cs" /> + <Compile Include="Events\SpecialisedEvents.cs" /> + <Compile Include="Framework\ContentPack.cs" /> <Compile Include="Framework\Content\ContentCache.cs" /> + <Compile Include="Framework\Events\ManagedEventBase.cs" /> <Compile Include="Framework\Input\InputState.cs" /> <Compile Include="Framework\Input\InputStatus.cs" /> <Compile Include="Framework\LegacyManifestVersion.cs" /> - <Compile Include="Framework\Models\ModCompatibility.cs" /> + <Compile Include="Framework\ModData\ModDatabase.cs" /> + <Compile Include="Framework\ModData\ModDataField.cs" /> + <Compile Include="Framework\ModData\ModDataFieldKey.cs" /> + <Compile Include="Framework\ModData\ParsedModDataRecord.cs" /> + <Compile Include="Framework\Models\ManifestContentPackFor.cs" /> <Compile Include="Framework\ModLoading\Finders\EventFinder.cs" /> <Compile Include="Framework\ModLoading\Finders\FieldFinder.cs" /> <Compile Include="Framework\ModLoading\Finders\MethodFinder.cs" /> @@ -110,15 +119,18 @@ <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> <Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" /> - <Compile Include="Framework\Serialisation\SmapiConverters\ModCompatibilityArrayConverter.cs" /> + <Compile Include="Framework\Reflection\InterfaceProxyFactory.cs" /> + <Compile Include="Framework\Serialisation\SmapiConverters\ManifestContentPackForConverter.cs" /> <Compile Include="Framework\Serialisation\SmapiConverters\ManifestDependencyArrayConverter.cs" /> - <Compile Include="Framework\Serialisation\SmapiConverters\ModDataIdConverter.cs" /> <Compile Include="Framework\Serialisation\SmapiConverters\SemanticVersionConverter.cs" /> <Compile Include="Framework\Serialisation\SimpleReadOnlyConverter.cs" /> <Compile Include="Framework\Serialisation\CrossplatformConverters\RectangleConverter.cs" /> <Compile Include="Framework\Serialisation\CrossplatformConverters\ColorConverter.cs" /> <Compile Include="Framework\Serialisation\CrossplatformConverters\PointConverter.cs" /> <Compile Include="Framework\Utilities\ContextHash.cs" /> + <Compile Include="Framework\Utilities\PathUtilities.cs" /> + <Compile Include="IContentPack.cs" /> + <Compile Include="IManifestContentPackFor.cs" /> <Compile Include="IReflectedField.cs" /> <Compile Include="IReflectedMethod.cs" /> <Compile Include="IReflectedProperty.cs" /> @@ -154,7 +166,6 @@ <Compile Include="Framework\Utilities\Countdown.cs" /> <Compile Include="Framework\GameVersion.cs" /> <Compile Include="Framework\IModMetadata.cs" /> - <Compile Include="Framework\Models\ModDataID.cs" /> <Compile Include="Framework\ModHelpers\BaseHelper.cs" /> <Compile Include="Framework\ModHelpers\CommandHelper.cs" /> <Compile Include="Framework\ModHelpers\ContentHelper.cs" /> @@ -177,7 +188,7 @@ <Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" /> <Compile Include="Framework\Logging\InterceptingTextWriter.cs" /> <Compile Include="Framework\Models\ManifestDependency.cs" /> - <Compile Include="Framework\Models\ModStatus.cs" /> + <Compile Include="Framework\ModData\ModStatus.cs" /> <Compile Include="Framework\Models\SConfig.cs" /> <Compile Include="Framework\ModLoading\ModMetadata.cs" /> <Compile Include="Framework\Reflection\ReflectedProperty.cs" /> @@ -205,7 +216,7 @@ <Compile Include="Framework\DeprecationLevel.cs" /> <Compile Include="Framework\DeprecationManager.cs" /> <Compile Include="Framework\InternalExtensions.cs" /> - <Compile Include="Framework\Models\ModDataRecord.cs" /> + <Compile Include="Framework\ModData\ModDataRecord.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoader.cs" /> <Compile Include="Framework\Reflection\CacheEntry.cs" /> <Compile Include="Framework\Reflection\ReflectedField.cs" /> @@ -250,9 +261,6 @@ <Content Include="StardewModdingAPI.config.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> - <None Include="unix-launcher.sh"> - <CopyToOutputDirectory>Always</CopyToOutputDirectory> - </None> </ItemGroup> <ItemGroup> <Content Include="icon.ico" /> diff --git a/src/SMAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh deleted file mode 100644 index 2542a286..00000000 --- a/src/SMAPI/unix-launcher.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -# MonoKickstart Shell Script -# Written by Ethan "flibitijibibo" Lee -# Modified for StardewModdingAPI by Viz and Pathoschild - -# Move to script's directory -cd "`dirname "$0"`" - -# Get the system architecture -UNAME=`uname` -ARCH=`uname -m` - -# MonoKickstart picks the right libfolder, so just execute the right binary. -if [ "$UNAME" == "Darwin" ]; then - # ... Except on OSX. - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ - - # El Capitan is a total idiot and wipes this variable out, making the - # Steam overlay disappear. This sidesteps "System Integrity Protection" - # and resets the variable with Valve's own variable (they provided this - # fix by the way, thanks Valve!). Note that you will need to update your - # launch configuration to the script location, NOT just the app location - # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). - # -flibit - if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then - export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" - fi - - # this was here before - ln -sf mcs.bin.osx mcs - - # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI - if [ -f libgdiplus.dylib ]; then - rm libgdiplus.dylib - fi - if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then - ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib - fi - - # launch SMAPI - cp StardewValley.bin.osx StardewModdingAPI.bin.osx - open -a Terminal ./StardewModdingAPI.bin.osx $@ -else - # choose launcher - LAUNCHER="" - if [ "$ARCH" == "x86_64" ]; then - ln -sf mcs.bin.x86_64 mcs - cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" - else - ln -sf mcs.bin.x86 mcs - cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86 $@" - fi - - # get cross-distro version of POSIX command - COMMAND="" - if command -v command 2>/dev/null; then - COMMAND="command -v" - elif type type 2>/dev/null; then - COMMAND="type" - fi - - # open SMAPI in terminal - if $COMMAND x-terminal-emulator 2>/dev/null; then - # Terminator converts -e to -x when used through x-terminal-emulator for some reason (per - # `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator - # is mapped to Terminator, invoke it directly instead. - if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then - terminator -e "$LAUNCHER" - else - x-terminal-emulator -e "$LAUNCHER" - fi - elif $COMMAND xfce4-terminal 2>/dev/null; then - xfce4-terminal -e "$LAUNCHER" - elif $COMMAND gnome-terminal 2>/dev/null; then - gnome-terminal -e "$LAUNCHER" - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" - elif $COMMAND konsole 2>/dev/null; then - konsole -e "$LAUNCHER" - elif $COMMAND terminal 2>/dev/null; then - terminal -e "$LAUNCHER" - else - $LAUNCHER - fi - - # some Linux users get error 127 (command not found) from the above block, even though - # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when - # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal - # (which can be slow if there is none). - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi -fi |