From 0e304e4d51857e3c7dc9cd18141176e44934755c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 May 2017 01:56:39 -0400 Subject: added basic context logging to simplify troubleshooting --- src/StardewModdingAPI/Framework/SGame.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 1f2bf3ac..dbc257e1 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1118,6 +1118,8 @@ namespace StardewModdingAPI.Framework { if (this.AfterLoadTimer == 0) { + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}'.", LogLevel.Trace); + SaveEvents.InvokeAfterLoad(this.Monitor); PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); TimeEvents.InvokeAfterDayStarted(this.Monitor); @@ -1132,6 +1134,7 @@ namespace StardewModdingAPI.Framework // after exit to title if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); SaveEvents.InvokeAfterReturnToTitle(this.Monitor); this.AfterLoadTimer = 5; this.IsExiting = false; @@ -1199,9 +1202,13 @@ namespace StardewModdingAPI.Framework // raise save events // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) if (newMenu is SaveGameMenu || newMenu is ShippingMenu) + { + this.Monitor.Log("Context: before save.", LogLevel.Trace); SaveEvents.InvokeBeforeSave(this.Monitor); + } else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) { + this.Monitor.Log("Context: after save, starting new day.", LogLevel.Trace); SaveEvents.InvokeAfterSave(this.Monitor); TimeEvents.InvokeAfterDayStarted(this.Monitor); } -- cgit From 8963793bf854bee368b5cd08a3e0eb410ee8e1a9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 May 2017 02:50:36 -0400 Subject: exit game after many consecutive unrecoverable draw errors (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index dbc257e1..f8226529 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -34,6 +34,12 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. + private readonly int MaxFailedDraws = 120; // roughly two seconds + + /// The number of consecutive failed draws. + private int FailedDraws = 0; + /// Whether the player has loaded a save and the world has finished initialising. private bool IsWorldReady => this.AfterLoadTimer < 0; @@ -944,9 +950,20 @@ namespace StardewModdingAPI.Framework } } } + + // reset failed draw count + this.FailedDraws = 0; } catch (Exception ex) { + // exit if irrecoverable + if (this.FailedDraws >= this.MaxFailedDraws) + { + this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); + return; + } + this.FailedDraws++; + // log error this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); -- cgit From 624840efe5f3d4135dafeb2939b182cfeb4ec6c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 May 2017 13:09:32 -0400 Subject: use more robust sprite batch recovery logic (#283) --- .../Framework/InternalExtensions.cs | 22 ++++++++++++++++++ src/StardewModdingAPI/Framework/SGame.cs | 27 +++++++++++----------- 2 files changed, 36 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index 5199c72d..cadf6598 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; namespace StardewModdingAPI.Framework { @@ -128,5 +130,25 @@ namespace StardewModdingAPI.Framework deprecationManager.Warn(modName, nounPhrase, version, severity); } } + + /**** + ** Sprite batch + ****/ + /// Get whether the sprite batch is between a begin and end pair. + /// The sprite batch to check. + /// The reflection helper with which to access private fields. + public static bool IsOpen(this SpriteBatch spriteBatch, IReflectionHelper reflection) + { + // get field name + const string fieldName = +#if SMAPI_FOR_WINDOWS + "inBeginEndPair"; +#else + "_beginCalled"; +#endif + + // get result + return reflection.GetPrivateValue(Game1.spriteBatch, fieldName); + } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f8226529..7dae937b 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework private readonly int MaxFailedDraws = 120; // roughly two seconds /// The number of consecutive failed draws. - private int FailedDraws = 0; + private int FailedDraws; /// Whether the player has loaded a save and the world has finished initialising. private bool IsWorldReady => this.AfterLoadTimer < 0; @@ -956,6 +956,9 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { + // log error + this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); + // exit if irrecoverable if (this.FailedDraws >= this.MaxFailedDraws) { @@ -964,22 +967,20 @@ namespace StardewModdingAPI.Framework } this.FailedDraws++; - // log error - this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); - - // fix sprite batch + // recover sprite batch try { - bool isSpriteBatchOpen = -#if SMAPI_FOR_WINDOWS - SGame.Reflection.GetPrivateValue(Game1.spriteBatch, "inBeginEndPair"); -#else - SGame.Reflection.GetPrivateValue(Game1.spriteBatch, "_beginCalled"); -#endif - if (isSpriteBatchOpen) + if (Game1.spriteBatch.IsOpen(SGame.Reflection)) { this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); - Game1.spriteBatch.End(); + try + { + Game1.spriteBatch.End(); + } + catch + { + Game1.spriteBatch = new SpriteBatch(this.GraphicsDevice); // sprite batch is broken, try replacing it + } } } catch (Exception innerEx) -- cgit From 72a0b4fc6d268c67c3fba171793d5864f9710276 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 May 2017 01:57:07 -0400 Subject: detect unrecoverable draw errors (#283) --- release-notes.md | 9 +++++++++ src/StardewModdingAPI/Framework/SGame.cs | 7 +++++++ 2 files changed, 16 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index af535895..78b83806 100644 --- a/release-notes.md +++ b/release-notes.md @@ -10,6 +10,15 @@ For mod developers: images). --> +## 1.13 +See [log](https://github.com/Pathoschild/SMAPI/compare/1.12...1.13). + +For players: +* SMAPI now has better draw error recovery and detects when the error is irrecoverable. + +For mod developers: +* SMAPI now logs basic context info to simplify troubleshooting. + ## 1.12 See [log](https://github.com/Pathoschild/SMAPI/compare/1.11...1.12). diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 7dae937b..6932af2a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -967,6 +967,13 @@ namespace StardewModdingAPI.Framework } this.FailedDraws++; + // abort in known unrecoverable cases + if (Game1.toolSpriteSheet?.IsDisposed == true) + { + this.Monitor.ExitGameImmediately("the game unexpectedly disposed the tool spritesheet, so it crashed trying to draw a tool. This is a known bug in Stardew Valley 1.2.29, and there's no way to recover from it."); + return; + } + // recover sprite batch try { -- cgit From 85f609dc6c2f02d89b9fccaacfe837f8822d6b7c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 May 2017 02:18:58 -0400 Subject: add optional verbose context logging --- release-notes.md | 2 +- src/StardewModdingAPI/Framework/Models/SConfig.cs | 3 ++ src/StardewModdingAPI/Framework/SGame.cs | 32 ++++++++++++++++++---- src/StardewModdingAPI/Program.cs | 3 ++ .../StardewModdingAPI.config.json | 5 ++++ 5 files changed, 39 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 78b83806..fc681138 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,7 +17,7 @@ For players: * SMAPI now has better draw error recovery and detects when the error is irrecoverable. For mod developers: -* SMAPI now logs basic context info to simplify troubleshooting. +* SMAPI now logs basic context info to simplify troubleshooting, and more detailed logging can be enabled by setting `VerboseLogging: true` in `StardewModdingAPI.config.json`. ## 1.12 See [log](https://github.com/Pathoschild/SMAPI/compare/1.11...1.12). diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index 0de96297..c3f0816e 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -12,6 +12,9 @@ /// Whether to check if a newer version of SMAPI is available on startup. public bool CheckForUpdates { get; set; } = true; + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } = false; + /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. public ModCompatibility[] ModCompatibility { get; set; } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 6932af2a..d248c3ca 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -30,13 +30,13 @@ namespace StardewModdingAPI.Framework /**** ** SMAPI state ****/ + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. + private readonly int MaxFailedDraws = 120; // roughly two seconds + /// The number of ticks until SMAPI should notify mods that the game has loaded. /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. - private readonly int MaxFailedDraws = 120; // roughly two seconds - /// The number of consecutive failed draws. private int FailedDraws; @@ -176,6 +176,12 @@ namespace StardewModdingAPI.Framework private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + /********* + ** Accessors + *********/ + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } + /********* ** Protected methods @@ -1133,6 +1139,9 @@ namespace StardewModdingAPI.Framework var oldValue = this.PreviousLocale; var newValue = LocalizedContentManager.CurrentLanguageCode; + if (this.VerboseLogging) + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + if (oldValue != null) ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); this.PreviousLocale = newValue; @@ -1143,7 +1152,7 @@ namespace StardewModdingAPI.Framework { if (this.AfterLoadTimer == 0) { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}'.", LogLevel.Trace); + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); SaveEvents.InvokeAfterLoad(this.Monitor); PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); @@ -1224,6 +1233,17 @@ namespace StardewModdingAPI.Framework IClickableMenu previousMenu = this.PreviousActiveMenu; IClickableMenu newMenu = Game1.activeClickableMenu; + // log context + if (this.VerboseLogging) + { + if (previousMenu == null) + this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); + else if (newMenu == null) + this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); + else + this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); + } + // raise save events // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) if (newMenu is SaveGameMenu || newMenu is ShippingMenu) @@ -1233,7 +1253,7 @@ namespace StardewModdingAPI.Framework } else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) { - this.Monitor.Log("Context: after save, starting new day.", LogLevel.Trace); + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); SaveEvents.InvokeAfterSave(this.Monitor); TimeEvents.InvokeAfterDayStarted(this.Monitor); } @@ -1262,6 +1282,8 @@ namespace StardewModdingAPI.Framework // raise current location changed if (Game1.currentLocation != this.PreviousGameLocation) { + 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.PreviousGameLocation = Game1.currentLocation; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 1e5fcfc3..1913544f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -230,6 +230,7 @@ namespace StardewModdingAPI { // load settings this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); @@ -266,6 +267,8 @@ namespace StardewModdingAPI this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + if (this.Settings.VerboseLogging) + this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace); // validate XNB integrity if (!this.ValidateContentIntegrity()) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 9438c621..f42a4dfc 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -21,6 +21,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "CheckForUpdates": true, + /** + * Whether SMAPI should log more information about the game context. + */ + "VerboseLogging": false, + /** * A list of mod versions SMAPI should consider compatible or broken regardless of whether it * detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. -- cgit From 486ac29796586e09540723dcae8070cf3e60285b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:11:39 -0400 Subject: use shared reflection helper --- src/StardewModdingAPI/Framework/ModHelper.cs | 7 ++++--- src/StardewModdingAPI/Framework/SGame.cs | 10 +++++++--- src/StardewModdingAPI/Program.cs | 8 ++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 09297a65..7810148c 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Framework public IContentHelper Content { get; } /// Simplifies access to private game code. - public IReflectionHelper Reflection { get; } = new ReflectionHelper(); + public IReflectionHelper Reflection { get; } /// Metadata about loaded mods. public IModRegistry ModRegistry { get; } @@ -44,9 +43,10 @@ namespace StardewModdingAPI.Framework /// Metadata about loaded mods. /// Manages console commands. /// The content manager which loads content assets. + /// Simplifies access to private game code. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager) + public ModHelper(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -64,6 +64,7 @@ namespace StardewModdingAPI.Framework this.Content = new ContentHelper(contentManager, modDirectory, manifest.Name); this.ModRegistry = modRegistry; this.ConsoleCommands = new CommandHelper(manifest.Name, commandManager); + this.Reflection = reflection; } /**** diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index d248c3ca..8786010e 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -9,7 +9,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Reflection; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -157,9 +156,11 @@ namespace StardewModdingAPI.Framework /**** ** Private wrappers ****/ + /// Simplifies access to private game code. + private static IReflectionHelper Reflection; + // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. - private static readonly IReflectionHelper Reflection = new ReflectionHelper(); private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static float _fps @@ -176,6 +177,7 @@ namespace StardewModdingAPI.Framework private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + /********* ** Accessors *********/ @@ -188,11 +190,13 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// Encapsulates monitoring and logging. - internal SGame(IMonitor monitor) + /// Simplifies access to private game code. + internal SGame(IMonitor monitor, IReflectionHelper reflection) { this.Monitor = monitor; this.FirstUpdate = true; SGame.Instance = this; + SGame.Reflection = reflection; Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 1913544f..aa78ff41 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -40,6 +41,9 @@ namespace StardewModdingAPI /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + /// Simplifies access to private game code. + private readonly IReflectionHelper Reflection = new ReflectionHelper(); + /// The underlying game instance. private SGame GameInstance; @@ -141,7 +145,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.GameInstance = new SGame(this.Monitor, this.Reflection); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -599,7 +603,7 @@ namespace StardewModdingAPI // inject data // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content); + mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; -- cgit From fa729fa70021106bfb8541695f5fb7c5654e31b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:14:59 -0400 Subject: don't try to recover from a completely broken sprite batch, which can cause a whole new set of problems (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 8786010e..b1ac7c58 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -990,14 +990,7 @@ namespace StardewModdingAPI.Framework if (Game1.spriteBatch.IsOpen(SGame.Reflection)) { this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); - try - { - Game1.spriteBatch.End(); - } - catch - { - Game1.spriteBatch = new SpriteBatch(this.GraphicsDevice); // sprite batch is broken, try replacing it - } + Game1.spriteBatch.End(); } } catch (Exception innerEx) -- cgit From 467d4a27ee565433559c9dc374f5c95107938498 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:16:04 -0400 Subject: reduce max consecutive draw crashes (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index b1ac7c58..81dae754 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework ** SMAPI state ****/ /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. - private readonly int MaxFailedDraws = 120; // roughly two seconds + private readonly int MaxFailedDraws = 60; // roughly one second /// The number of ticks until SMAPI should notify mods that the game has loaded. /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. -- cgit From b4584afda897b695c5fc306db9facc0690b5ab75 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 22:37:53 -0400 Subject: trace locale changes as non-verbose context --- src/StardewModdingAPI/Framework/SGame.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 81dae754..f0450306 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1136,8 +1136,7 @@ namespace StardewModdingAPI.Framework var oldValue = this.PreviousLocale; var newValue = LocalizedContentManager.CurrentLanguageCode; - if (this.VerboseLogging) - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); if (oldValue != null) ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); -- cgit From 86c60c971ae533321b6550f140af4f01351cb2e3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 10 May 2017 23:49:58 -0400 Subject: merge SGame::UpdateEventCalls into Update The method was misleadingly named (since only some of the events were in the method), and unnecessarily limited the possible flows. --- src/StardewModdingAPI/Framework/SGame.cs | 501 +++++++++++++++---------------- 1 file changed, 247 insertions(+), 254 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f0450306..ed1ff647 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -257,8 +257,253 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeGameLoaded(this.Monitor); } - // update SMAPI events - this.UpdateEventCalls(); + // content locale changed event + if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) + { + var oldValue = this.PreviousLocale; + var newValue = LocalizedContentManager.CurrentLanguageCode; + + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + + if (oldValue != null) + ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.PreviousLocale = newValue; + } + + // save loaded event + if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) + { + if (this.AfterLoadTimer == 0) + { + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + + SaveEvents.InvokeAfterLoad(this.Monitor); + PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + this.AfterLoadTimer--; + } + + // before exit to title + if (Game1.exitToTitle) + this.IsExiting = true; + + // after exit to title + if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) + { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + this.AfterLoadTimer = 5; + this.IsExiting = false; + } + + // input events + { + // get latest state + this.KStateNow = Keyboard.GetState(); + this.MStateNow = Mouse.GetState(); + this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); + + // raise key pressed + foreach (var key in this.FramePressedKeys) + ControlEvents.InvokeKeyPressed(this.Monitor, key); + + // raise key released + foreach (var key in this.FrameReleasedKeys) + ControlEvents.InvokeKeyReleased(this.Monitor, key); + + // raise controller button pressed + for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + { + var buttons = this.GetFramePressedButtons(i); + foreach (var button in buttons) + { + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) + ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + else + ControlEvents.InvokeButtonPressed(this.Monitor, i, button); + } + } + + // raise controller button released + for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + { + foreach (var button in this.GetFrameReleasedButtons(i)) + { + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) + ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + else + ControlEvents.InvokeButtonReleased(this.Monitor, i, button); + } + } + + // raise keyboard state changed + if (this.KStateNow != this.KStatePrior) + ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); + + // raise mouse state changed + if (this.MStateNow != this.MStatePrior) + { + ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); + this.MStatePrior = this.MStateNow; + this.MPositionPrior = this.MPositionNow; + } + } + + // menu events + if (Game1.activeClickableMenu != this.PreviousActiveMenu) + { + IClickableMenu previousMenu = this.PreviousActiveMenu; + IClickableMenu newMenu = Game1.activeClickableMenu; + + // log context + if (this.VerboseLogging) + { + if (previousMenu == null) + this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); + else if (newMenu == null) + this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); + else + this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); + } + + // raise save events + // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) + if (newMenu is SaveGameMenu || newMenu is ShippingMenu) + { + this.Monitor.Log("Context: before save.", LogLevel.Trace); + SaveEvents.InvokeBeforeSave(this.Monitor); + } + else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) + { + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + SaveEvents.InvokeAfterSave(this.Monitor); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + + // raise menu events + if (newMenu != null) + MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + else + MenuEvents.InvokeMenuClosed(this.Monitor, 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) + this.PreviousActiveMenu = newMenu; + } + + // world & player events + if (this.IsWorldReady) + { + // raise location list changed + if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + { + LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + this.PreviousGameLocations = this.GetHash(Game1.locations); + } + + // raise current location changed + if (Game1.currentLocation != this.PreviousGameLocation) + { + 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.PreviousGameLocation = Game1.currentLocation; + } + + // raise player changed + if (Game1.player != this.PreviousFarmer) + { + PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); + this.PreviousFarmer = Game1.player; + } + + // raise player leveled up a skill + if (Game1.player.combatLevel != this.PreviousCombatLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + this.PreviousCombatLevel = Game1.player.combatLevel; + } + if (Game1.player.farmingLevel != this.PreviousFarmingLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + this.PreviousFarmingLevel = Game1.player.farmingLevel; + } + if (Game1.player.fishingLevel != this.PreviousFishingLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + this.PreviousFishingLevel = Game1.player.fishingLevel; + } + if (Game1.player.foragingLevel != this.PreviousForagingLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + this.PreviousForagingLevel = Game1.player.foragingLevel; + } + if (Game1.player.miningLevel != this.PreviousMiningLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + this.PreviousMiningLevel = Game1.player.miningLevel; + } + if (Game1.player.luckLevel != this.PreviousLuckLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); + this.PreviousLuckLevel = 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.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); + } + + // raise current location's object list changed + { + int? objectHash = Game1.currentLocation?.objects != null ? this.GetHash(Game1.currentLocation.objects) : (int?)null; + if (objectHash != null && this.PreviousLocationObjects != objectHash) + { + LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + this.PreviousLocationObjects = objectHash.Value; + } + } + + // raise time changed + if (Game1.timeOfDay != this.PreviousTime) + { + TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + this.PreviousTime = Game1.timeOfDay; + } + if (Game1.dayOfMonth != this.PreviousDay) + { + TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); + this.PreviousDay = Game1.dayOfMonth; + } + if (Game1.currentSeason != this.PreviousSeason) + { + TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); + this.PreviousSeason = Game1.currentSeason; + } + if (Game1.year != this.PreviousYear) + { + TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); + this.PreviousYear = Game1.year; + } + + // raise mine level changed + if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) + { + MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + this.PreviousMineLevel = Game1.mine.mineLevel; + } + } + + // raise game day transition event (obsolete) + if (Game1.newDay != this.PreviousIsNewDay) + { + TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); + this.PreviousIsNewDay = Game1.newDay; + } // let game update try @@ -1127,258 +1372,6 @@ namespace StardewModdingAPI.Framework return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released, stateIndex); } - /// Detect changes since the last update ticket and trigger mod events. - private void UpdateEventCalls() - { - // content locale changed event - if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) - { - var oldValue = this.PreviousLocale; - var newValue = LocalizedContentManager.CurrentLanguageCode; - - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); - - if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); - this.PreviousLocale = newValue; - } - - // save loaded event - if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) - { - if (this.AfterLoadTimer == 0) - { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - - SaveEvents.InvokeAfterLoad(this.Monitor); - PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - this.AfterLoadTimer--; - } - - // before exit to title - if (Game1.exitToTitle) - this.IsExiting = true; - - // after exit to title - if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) - { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); - this.AfterLoadTimer = 5; - this.IsExiting = false; - } - - // input events - { - // get latest state - this.KStateNow = Keyboard.GetState(); - this.MStateNow = Mouse.GetState(); - this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); - - // raise key pressed - foreach (var key in this.FramePressedKeys) - ControlEvents.InvokeKeyPressed(this.Monitor, key); - - // raise key released - foreach (var key in this.FrameReleasedKeys) - ControlEvents.InvokeKeyReleased(this.Monitor, key); - - // raise controller button pressed - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - { - var buttons = this.GetFramePressedButtons(i); - foreach (var button in buttons) - { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonPressed(this.Monitor, i, button); - } - } - - // raise controller button released - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - { - foreach (var button in this.GetFrameReleasedButtons(i)) - { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonReleased(this.Monitor, i, button); - } - } - - // raise keyboard state changed - if (this.KStateNow != this.KStatePrior) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); - - // raise mouse state changed - if (this.MStateNow != this.MStatePrior) - { - ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); - this.MStatePrior = this.MStateNow; - this.MPositionPrior = this.MPositionNow; - } - } - - // menu events - if (Game1.activeClickableMenu != this.PreviousActiveMenu) - { - IClickableMenu previousMenu = this.PreviousActiveMenu; - IClickableMenu newMenu = Game1.activeClickableMenu; - - // log context - if (this.VerboseLogging) - { - if (previousMenu == null) - this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); - else if (newMenu == null) - this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); - else - this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); - } - - // raise save events - // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) - if (newMenu is SaveGameMenu || newMenu is ShippingMenu) - { - this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); - } - else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) - { - this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - - // raise menu events - if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); - else - MenuEvents.InvokeMenuClosed(this.Monitor, 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) - this.PreviousActiveMenu = newMenu; - } - - // world & player events - if (this.IsWorldReady) - { - // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - { - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - this.PreviousGameLocations = this.GetHash(Game1.locations); - } - - // raise current location changed - if (Game1.currentLocation != this.PreviousGameLocation) - { - 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.PreviousGameLocation = Game1.currentLocation; - } - - // raise player changed - if (Game1.player != this.PreviousFarmer) - { - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); - this.PreviousFarmer = Game1.player; - } - - // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); - this.PreviousCombatLevel = Game1.player.combatLevel; - } - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); - this.PreviousFarmingLevel = Game1.player.farmingLevel; - } - if (Game1.player.fishingLevel != this.PreviousFishingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); - this.PreviousFishingLevel = Game1.player.fishingLevel; - } - if (Game1.player.foragingLevel != this.PreviousForagingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); - this.PreviousForagingLevel = Game1.player.foragingLevel; - } - if (Game1.player.miningLevel != this.PreviousMiningLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); - this.PreviousMiningLevel = Game1.player.miningLevel; - } - if (Game1.player.luckLevel != this.PreviousLuckLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); - this.PreviousLuckLevel = 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.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); - } - - // raise current location's object list changed - { - int? objectHash = Game1.currentLocation?.objects != null ? this.GetHash(Game1.currentLocation.objects) : (int?)null; - if (objectHash != null && this.PreviousLocationObjects != objectHash) - { - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); - this.PreviousLocationObjects = objectHash.Value; - } - } - - // raise time changed - if (Game1.timeOfDay != this.PreviousTime) - { - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); - this.PreviousTime = Game1.timeOfDay; - } - if (Game1.dayOfMonth != this.PreviousDay) - { - TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); - this.PreviousDay = Game1.dayOfMonth; - } - if (Game1.currentSeason != this.PreviousSeason) - { - TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); - this.PreviousSeason = Game1.currentSeason; - } - if (Game1.year != this.PreviousYear) - { - TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); - this.PreviousYear = Game1.year; - } - - // raise mine level changed - if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - { - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); - this.PreviousMineLevel = Game1.mine.mineLevel; - } - } - - // raise game day transition event (obsolete) - if (Game1.newDay != this.PreviousIsNewDay) - { - TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); - this.PreviousIsNewDay = Game1.newDay; - } - } - /// Get the player inventory changes between two states. /// The player's current inventory. /// The player's previous inventory. -- cgit From 48c5c9e36794ed3dae0cf6114194b0dc80dd4725 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 May 2017 00:34:01 -0400 Subject: overhaul save handling to fix save events not triggering on Linux/Mac (#284) --- release-notes.md | 1 + src/StardewModdingAPI/Context.cs | 2 +- src/StardewModdingAPI/Framework/SGame.cs | 89 +++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 30 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index e6e594a5..f48a5e9d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,6 +17,7 @@ For players: * SMAPI now has better draw error recovery and detects when the error is irrecoverable. * SMAPI now remembers if your game crashed and offers help next time you relaunch. * Fixed installer finding redundant game paths on Linux. +* Fixed save events not being raised after the first day on Linux/Mac. For mod developers: * Added log entries for basic context changes (e.g. loaded save) to simplify troubleshooting. More detailed logging can be enabled by setting `VerboseLogging: true` in `StardewModdingAPI.config.json`. diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 2da14eed..45f6a05f 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI public static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); /// Whether the game is currently writing to the save file. - public static bool IsSaving => SaveGame.IsProcessing && (Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu); // IsProcessing is never set to false on Linux/Mac + public static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something /// Whether the game is currently running the draw loop. public static bool IsInDrawLoop { get; set; } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index ed1ff647..f2098dbd 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -45,6 +45,9 @@ namespace StardewModdingAPI.Framework /// Whether the game is returning to the menu. private bool IsExiting; + /// Whether the game is saving and SMAPI has already raised . + private bool IsBetweenSaveEvents; + /// Whether the game's zoom level is at 100% (i.e. nothing should be scaled). public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f); @@ -216,6 +219,9 @@ namespace StardewModdingAPI.Framework /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) { + /********* + ** Skip conditions + *********/ // SMAPI exiting, stop processing game updates if (this.Monitor.IsExiting) { @@ -242,14 +248,34 @@ namespace StardewModdingAPI.Framework // While the game is writing to the save file in the background, mods can unexpectedly // fail since they don't have exclusive access to resources (e.g. collection changed // during enumeration errors). To avoid problems, events are not invoked while a save - // is in progress. + // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is + // opened (since the save hasn't started yet), but all other events should be suppressed. if (Context.IsSaving) { + // raise before-save + if (!this.IsBetweenSaveEvents) + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save.", LogLevel.Trace); + SaveEvents.InvokeBeforeSave(this.Monitor); + } + + // suppress non-save events base.Update(gameTime); return; } + 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); + } - // raise game loaded + /********* + ** Game loaded events + *********/ if (this.FirstUpdate) { GameEvents.InvokeInitialize(this.Monitor); @@ -257,7 +283,9 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeGameLoaded(this.Monitor); } - // content locale changed event + /********* + ** Locale changed events + *********/ if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) { var oldValue = this.PreviousLocale; @@ -270,7 +298,9 @@ namespace StardewModdingAPI.Framework this.PreviousLocale = newValue; } - // save loaded event + /********* + ** After load events + *********/ if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) { if (this.AfterLoadTimer == 0) @@ -284,6 +314,9 @@ namespace StardewModdingAPI.Framework this.AfterLoadTimer--; } + /********* + ** Exit to title events + *********/ // before exit to title if (Game1.exitToTitle) this.IsExiting = true; @@ -297,7 +330,9 @@ namespace StardewModdingAPI.Framework this.IsExiting = false; } - // input events + /********* + ** Input events + *********/ { // get latest state this.KStateNow = Keyboard.GetState(); @@ -350,7 +385,9 @@ namespace StardewModdingAPI.Framework } } - // menu events + /********* + ** Menu events + *********/ if (Game1.activeClickableMenu != this.PreviousActiveMenu) { IClickableMenu previousMenu = this.PreviousActiveMenu; @@ -367,20 +404,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); } - // raise save events - // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) - if (newMenu is SaveGameMenu || newMenu is ShippingMenu) - { - this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); - } - else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) - { - this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - // raise menu events if (newMenu != null) MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); @@ -392,7 +415,9 @@ namespace StardewModdingAPI.Framework this.PreviousActiveMenu = newMenu; } - // world & player events + /********* + ** World & player events + *********/ if (this.IsWorldReady) { // raise location list changed @@ -498,14 +523,18 @@ namespace StardewModdingAPI.Framework } } - // raise game day transition event (obsolete) + /********* + ** Game day transition event (obsolete) + *********/ if (Game1.newDay != this.PreviousIsNewDay) { TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); this.PreviousIsNewDay = Game1.newDay; } - // let game update + /********* + ** Game update + *********/ try { base.Update(gameTime); @@ -514,8 +543,10 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } - - // raise update events + + /********* + ** Update events + *********/ GameEvents.InvokeUpdateTick(this.Monitor); if (this.FirstUpdate) { @@ -538,11 +569,11 @@ namespace StardewModdingAPI.Framework if (this.CurrentUpdateTick >= 60) this.CurrentUpdateTick = 0; - // track keyboard state + /********* + ** Update input state + *********/ this.KStatePrior = this.KStateNow; - - // track controller button state - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + for (PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++) this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i); } -- cgit From 03876153f4fbb13b8a260c529513a306319f9e05 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 May 2017 22:25:45 -0400 Subject: decouple mod metadata vs assembly loading to enable upcoming mod dependencies (#285) --- src/StardewModdingAPI/Framework/ModMetadata.cs | 40 +++++++ src/StardewModdingAPI/Program.cs | 139 +++++++++++++++---------- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 124 insertions(+), 56 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModMetadata.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModMetadata.cs new file mode 100644 index 00000000..aeb9261a --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModMetadata.cs @@ -0,0 +1,40 @@ +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework +{ + /// Metadata for a mod. + internal class ModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string DisplayName { get; } + + /// The mod's full directory path. + public string DirectoryPath { get; } + + /// The mod manifest. + public IManifest Manifest { get; } + + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModCompatibility Compatibility { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's display name. + /// The mod's full directory path. + /// The mod manifest. + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + { + this.DisplayName = displayName; + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Compatibility = compatibility; + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 70b2fbc1..a5bd7788 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -310,7 +310,18 @@ namespace StardewModdingAPI this.Monitor.Log("SMAPI found problems in the game's XNB files which may cause errors or crashes while you're playing. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Warn); // load mods - int modsLoaded = this.LoadMods(); + int modsLoaded; + { + // load mods + JsonHelper jsonHelper = new JsonHelper(); + IList deprecationWarnings = new List(); + ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); + modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); + + // log deprecation warnings together + foreach (Action warning in deprecationWarnings) + warning(); + } if (this.Monitor.IsExiting) { this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); @@ -444,32 +455,27 @@ namespace StardewModdingAPI } } - /// Load and hook up all mods in the mod directory. - /// Returns the number of mods loaded. - private int LoadMods() + /// Find all mods in the given folder. + /// The root mod path to search. + /// The JSON helper with which to read the manifest file. + /// A list to populate with any deprecation warnings. + private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList deprecationWarnings) { - this.Monitor.Log("Loading mods..."); - - // get JSON helper - JsonHelper jsonHelper = new JsonHelper(); - - // get assembly loader - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + this.Monitor.Log("Finding mods..."); + void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); - // load mod assemblies - int modsLoaded = 0; - List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list - foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath)) + // load mod metadata + List mods = new List(); + foreach (string modRootPath in Directory.GetDirectories(rootPath)) { if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn); - return modsLoaded; - } + return new ModMetadata[0]; // exit in progress + + // init metadata + string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); // passthrough empty directories - DirectoryInfo directory = new DirectoryInfo(directoryPath); + DirectoryInfo directory = new DirectoryInfo(modRootPath); while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) directory = directory.GetDirectories().First(); @@ -477,35 +483,34 @@ namespace StardewModdingAPI string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { - this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); + LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); continue; } - string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}"; // read manifest Manifest manifest; try { - // read manifest text + // read manifest file string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { - this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error); + LogSkip(displayName, "its manifest is empty."); continue; } - // deserialise manifest + // parse manifest manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); if (manifest == null) { - this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); + LogSkip(displayName, "its manifest is invalid."); continue; } // validate manifest if (string.IsNullOrWhiteSpace(manifest.EntryDll)) { - this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error); + LogSkip(displayName, "its manifest doesn't set an entry DLL."); continue; } if (string.IsNullOrWhiteSpace(manifest.UniqueID)) @@ -513,11 +518,11 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); continue; } if (!string.IsNullOrWhiteSpace(manifest.Name)) - skippedPrefix = $"Skipped {manifest.Name}"; + displayName = manifest.Name; // validate compatibility ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest); @@ -527,14 +532,13 @@ namespace StardewModdingAPI bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; - string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; if (hasOfficialUrl) - warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; if (hasUnofficialUrl) - warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - this.Monitor.Log(warning, LogLevel.Error); - continue; + LogSkip(displayName, error); } // validate SMAPI version @@ -545,13 +549,13 @@ namespace StardewModdingAPI ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { - this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); + LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } } catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { - this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); + LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); continue; } } @@ -566,39 +570,66 @@ namespace StardewModdingAPI Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { - this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error); + LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); continue; } } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); continue; } } - // validate mod path to simplify errors + // validate DLL path string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { - this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error); + LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); continue; } + // add mod metadata + mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); + } + + return mods.ToArray(); + } + + /// Load and hook up the given mods. + /// The mods to load. + /// The JSON helper with which to read mods' JSON files. + /// The content manager to use for mod content. + /// A list to populate with any deprecation warnings. + /// Returns the number of mods successfully loaded. + private int LoadMods(ModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) + { + this.Monitor.Log("Loading mods..."); + void LogSkip(ModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level); + + // load mod assemblies + int modsLoaded = 0; + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); + foreach (ModMetadata metadata in mods) + { + IManifest manifest = metadata.Manifest; + string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll); + // preprocess & load mod assembly Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { - this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error); + LogSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version})."); continue; } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } @@ -608,18 +639,18 @@ namespace StardewModdingAPI int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); if (modEntries == 0) { - this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error); + LogSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); continue; } if (modEntries > 1) { - this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error); + LogSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); continue; } } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } @@ -631,16 +662,15 @@ namespace StardewModdingAPI Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { - this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated."); + LogSkip(metadata, "its entry class couldn't be instantiated."); continue; } // inject data - // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content, this.Reflection); + mod.Helper = new ModHelper(manifest, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); - mod.PathOnDisk = directory.FullName; + mod.PathOnDisk = metadata.DirectoryPath; // track mod this.ModRegistry.Add(mod); @@ -649,11 +679,11 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error); + LogSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); } } - // initialise mods + // initialise loaded mods foreach (IMod mod in this.ModRegistry.GetMods()) { try @@ -674,9 +704,6 @@ namespace StardewModdingAPI // print result this.Monitor.Log($"Loaded {modsLoaded} mods."); - foreach (Action warning in deprecationWarnings) - warning(); - return modsLoaded; } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 87ce65b0..69c167c9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -158,6 +158,7 @@ + -- cgit From bb165f2079e33d02c0e673db73ac5b336272a3fa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 11 May 2017 23:21:02 -0400 Subject: organise a few framework classes --- .../Framework/AssemblyDefinitionResolver.cs | 61 ----- src/StardewModdingAPI/Framework/AssemblyLoader.cs | 292 --------------------- .../Framework/AssemblyParseResult.cs | 31 --- src/StardewModdingAPI/Framework/Manifest.cs | 45 ---- .../ModLoading/AssemblyDefinitionResolver.cs | 61 +++++ .../Framework/ModLoading/AssemblyLoader.cs | 292 +++++++++++++++++++++ .../Framework/ModLoading/AssemblyParseResult.cs | 31 +++ .../Framework/ModLoading/ModMetadata.cs | 40 +++ src/StardewModdingAPI/Framework/ModMetadata.cs | 40 --- src/StardewModdingAPI/Framework/Models/Manifest.cs | 44 ++++ src/StardewModdingAPI/Mod.cs | 1 + src/StardewModdingAPI/Program.cs | 1 + src/StardewModdingAPI/StardewModdingAPI.csproj | 10 +- 13 files changed, 475 insertions(+), 474 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs delete mode 100644 src/StardewModdingAPI/Framework/AssemblyLoader.cs delete mode 100644 src/StardewModdingAPI/Framework/AssemblyParseResult.cs delete mode 100644 src/StardewModdingAPI/Framework/Manifest.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs delete mode 100644 src/StardewModdingAPI/Framework/ModMetadata.cs create mode 100644 src/StardewModdingAPI/Framework/Models/Manifest.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs deleted file mode 100644 index b4e69fcd..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyDefinitionResolver.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework -{ - /// A minimal assembly definition resolver which resolves references to known assemblies. - internal class AssemblyDefinitionResolver : DefaultAssemblyResolver - { - /********* - ** Properties - *********/ - /// The known assemblies. - private readonly IDictionary Loaded = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Add known assemblies to the resolver. - /// The known assemblies. - public void Add(params AssemblyDefinition[] assemblies) - { - foreach (AssemblyDefinition assembly in assemblies) - { - this.Loaded[assembly.Name.Name] = assembly; - this.Loaded[assembly.Name.FullName] = assembly; - } - } - - /// Resolve an assembly reference. - /// The assembly name. - public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); - - /// Resolve an assembly reference. - /// The assembly name. - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); - - /// Resolve an assembly reference. - /// The assembly full name (including version, etc). - /// The assembly reader parameters. - public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); - - - /********* - ** Private methods - *********/ - /// Resolve a known assembly definition based on its short or full name. - /// The assembly's short or full name. - private AssemblyDefinition ResolveName(string name) - { - return this.Loaded.ContainsKey(name) - ? this.Loaded[name] - : null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs deleted file mode 100644 index 2c9973c1..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters; - -namespace StardewModdingAPI.Framework -{ - /// Preprocesses and loads mod assemblies. - internal class AssemblyLoader - { - /********* - ** Properties - *********/ - /// Metadata for mapping assemblies to the current platform. - private readonly PlatformAssemblyMap AssemblyMap; - - /// A type => assembly lookup for types which should be rewritten. - private readonly IDictionary TypeAssemblies; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The current game platform. - /// Encapsulates monitoring and logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor) - { - this.Monitor = monitor; - this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); - - // generate type => assembly lookup for types which should be rewritten - this.TypeAssemblies = new Dictionary(); - foreach (Assembly assembly in this.AssemblyMap.Targets) - { - ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; - foreach (TypeDefinition type in module.GetTypes()) - { - if (!type.IsPublic) - continue; // no need to rewrite - if (type.Namespace.Contains("<")) - continue; // ignore assembly metadata - this.TypeAssemblies[type.FullName] = assembly; - } - } - } - - /// Preprocess and load an assembly. - /// The assembly file path. - /// Assume the mod is compatible, even if incompatible code is detected. - /// Returns the rewrite metadata for the preprocessed assembly. - /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(string assemblyPath, bool assumeCompatible) - { - // get referenced local assemblies - AssemblyParseResult[] assemblies; - { - AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); - HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded - assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); - if (!assemblies.Any()) - throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); - resolver.Add(assemblies.Select(p => p.Definition).ToArray()); - } - - // rewrite & load assemblies in leaf-to-root order - Assembly lastAssembly = null; - foreach (AssemblyParseResult assembly in assemblies) - { - bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); - if (changed) - { - this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } - } - else - { - this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); - lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); - } - } - - // last assembly loaded is the root - return lastAssembly; - } - - /// Resolve an assembly by its name. - /// The assembly name. - /// - /// This implementation returns the first loaded assembly which matches the short form of - /// the assembly name, to resolve assembly resolution issues when rewriting - /// assemblies (especially with Mono). Since this is meant to be called on , - /// the implicit assumption is that loading the exact assembly failed. - /// - public Assembly ResolveAssembly(string name) - { - string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) - return AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(p => p.GetName().Name == shortName); - } - - - /********* - ** Private methods - *********/ - /**** - ** Assembly parsing - ****/ - /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. - /// The assembly file to load. - /// The assembly names that should be skipped. - /// A resolver which resolves references to known assemblies. - /// Returns the rewrite metadata for the preprocessed assembly. - private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) - { - // validate - if (file.Directory == null) - throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); - if (!file.Exists) - yield break; // not a local assembly - - // read assembly - byte[] assemblyBytes = File.ReadAllBytes(file.FullName); - AssemblyDefinition assembly; - using (Stream readStream = new MemoryStream(assemblyBytes)) - assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); - - // skip if already visited - if (visitedAssemblyNames.Contains(assembly.Name.Name)) - yield break; - visitedAssemblyNames.Add(assembly.Name.Name); - - // yield referenced assemblies - foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) - { - FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); - foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) - yield return result; - } - - // yield assembly - yield return new AssemblyParseResult(file, assembly); - } - - /**** - ** Assembly rewriting - ****/ - /// Rewrite the types referenced by an assembly. - /// The assembly to rewrite. - /// Assume the mod is compatible, even if incompatible code is detected. - /// Returns whether the assembly was modified. - /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) - { - ModuleDefinition module = assembly.MainModule; - HashSet loggedMessages = new HashSet(); - - // swap assembly references if needed (e.g. XNA => MonoGame) - bool platformChanged = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) - { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) - { - this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; - } - } - if (platformChanged) - { - // add target assembly references - foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) - module.AssemblyReferences.Add(target); - - // rewrite type scopes to use target assemblies - IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); - } - - // find (and optionally rewrite) incompatible instructions - bool anyRewritten = false; - IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) - { - // check method definition - foreach (IInstructionRewriter rewriter in rewriters) - { - try - { - if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) - { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } - } - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - foreach (Instruction instruction in cil.Body.Instructions.ToArray()) - { - foreach (IInstructionRewriter rewriter in rewriters) - { - try - { - if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) - { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } - } - } - } - - return platformChanged || anyRewritten; - } - - /// Get the correct reference to use for compatibility with the current platform. - /// The type reference to rewrite. - private void ChangeTypeScope(TypeReference type) - { - // check skip conditions - if (type == null || type.FullName.StartsWith("System.")) - return; - - // get assembly - Assembly assembly; - if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) - return; - - // replace scope - AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; - type.Scope = assemblyRef; - } - - /// Get all methods in a module. - /// The module to search. - private IEnumerable GetMethods(ModuleDefinition module) - { - return ( - from type in module.GetTypes() - where type.HasMethods - from method in type.Methods - where method.HasBody - select method - ); - } - - /// Log a message for the player or developer the first time it occurs. - /// The monitor through which to log the message. - /// The hash of logged messages. - /// The message to log. - /// The log severity level. - private void LogOnce(IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) - { - if (!hash.Contains(message)) - { - monitor.Log(message, level); - hash.Add(message); - } - } - } -} diff --git a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/AssemblyParseResult.cs deleted file mode 100644 index bff976aa..00000000 --- a/src/StardewModdingAPI/Framework/AssemblyParseResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using Mono.Cecil; - -namespace StardewModdingAPI.Framework -{ - /// Metadata about a parsed assembly definition. - internal class AssemblyParseResult - { - /********* - ** Accessors - *********/ - /// The original assembly file. - public readonly FileInfo File; - - /// The assembly definition. - public readonly AssemblyDefinition Definition; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The original assembly file. - /// The assembly definition. - public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) - { - this.File = file; - this.Definition = assembly; - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Manifest.cs b/src/StardewModdingAPI/Framework/Manifest.cs deleted file mode 100644 index 62c711e2..00000000 --- a/src/StardewModdingAPI/Framework/Manifest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework -{ - /// A manifest which describes a mod for SMAPI. - internal class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// A brief description of the mod. - public string Description { get; set; } - - /// The mod author's name. - public string Author { get; set; } - - /// The mod version. - [JsonConverter(typeof(SemanticVersionConverter))] - public ISemanticVersion Version { get; set; } - - /// The minimum SMAPI version required by this mod, if any. - public string MinimumApiVersion { get; set; } - - /// The name of the DLL in the directory that has the method. - public string EntryDll { get; set; } - - /// The unique mod ID. - public string UniqueID { get; set; } - - /// Whether the mod uses per-save config files. - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public bool PerSaveConfigs { get; set; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs new file mode 100644 index 00000000..4378798c --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// A minimal assembly definition resolver which resolves references to known assemblies. + internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + { + /********* + ** Properties + *********/ + /// The known assemblies. + private readonly IDictionary Loaded = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Add known assemblies to the resolver. + /// The known assemblies. + public void Add(params AssemblyDefinition[] assemblies) + { + foreach (AssemblyDefinition assembly in assemblies) + { + this.Loaded[assembly.Name.Name] = assembly; + this.Loaded[assembly.Name.FullName] = assembly; + } + } + + /// Resolve an assembly reference. + /// The assembly name. + public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); + + /// Resolve an assembly reference. + /// The assembly name. + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); + + /// Resolve an assembly reference. + /// The assembly full name (including version, etc). + public override AssemblyDefinition Resolve(string fullName) => this.ResolveName(fullName) ?? base.Resolve(fullName); + + /// Resolve an assembly reference. + /// The assembly full name (including version, etc). + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(string fullName, ReaderParameters parameters) => this.ResolveName(fullName) ?? base.Resolve(fullName, parameters); + + + /********* + ** Private methods + *********/ + /// Resolve a known assembly definition based on its short or full name. + /// The assembly's short or full name. + private AssemblyDefinition ResolveName(string name) + { + return this.Loaded.ContainsKey(name) + ? this.Loaded[name] + : null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs new file mode 100644 index 00000000..42bd7bfb --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.AssemblyRewriters; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Preprocesses and loads mod assemblies. + internal class AssemblyLoader + { + /********* + ** Properties + *********/ + /// Metadata for mapping assemblies to the current platform. + private readonly PlatformAssemblyMap AssemblyMap; + + /// A type => assembly lookup for types which should be rewritten. + private readonly IDictionary TypeAssemblies; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current game platform. + /// Encapsulates monitoring and logging. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor) + { + this.Monitor = monitor; + this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); + + // generate type => assembly lookup for types which should be rewritten + this.TypeAssemblies = new Dictionary(); + foreach (Assembly assembly in this.AssemblyMap.Targets) + { + ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; + foreach (TypeDefinition type in module.GetTypes()) + { + if (!type.IsPublic) + continue; // no need to rewrite + if (type.Namespace.Contains("<")) + continue; // ignore assembly metadata + this.TypeAssemblies[type.FullName] = assembly; + } + } + } + + /// Preprocess and load an assembly. + /// The assembly file path. + /// Assume the mod is compatible, even if incompatible code is detected. + /// Returns the rewrite metadata for the preprocessed assembly. + /// An incompatible CIL instruction was found while rewriting the assembly. + public Assembly Load(string assemblyPath, bool assumeCompatible) + { + // get referenced local assemblies + AssemblyParseResult[] assemblies; + { + AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); + HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded + assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); + if (!assemblies.Any()) + throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); + resolver.Add(assemblies.Select(p => p.Definition).ToArray()); + } + + // rewrite & load assemblies in leaf-to-root order + Assembly lastAssembly = null; + foreach (AssemblyParseResult assembly in assemblies) + { + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); + if (changed) + { + this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + { + this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + } + } + + // last assembly loaded is the root + return lastAssembly; + } + + /// Resolve an assembly by its name. + /// The assembly name. + /// + /// This implementation returns the first loaded assembly which matches the short form of + /// the assembly name, to resolve assembly resolution issues when rewriting + /// assemblies (especially with Mono). Since this is meant to be called on , + /// the implicit assumption is that loading the exact assembly failed. + /// + public Assembly ResolveAssembly(string name) + { + string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) + return AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(p => p.GetName().Name == shortName); + } + + + /********* + ** Private methods + *********/ + /**** + ** Assembly parsing + ****/ + /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. + /// The assembly file to load. + /// The assembly names that should be skipped. + /// A resolver which resolves references to known assemblies. + /// Returns the rewrite metadata for the preprocessed assembly. + private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) + { + // validate + if (file.Directory == null) + throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); + if (!file.Exists) + yield break; // not a local assembly + + // read assembly + byte[] assemblyBytes = File.ReadAllBytes(file.FullName); + AssemblyDefinition assembly; + using (Stream readStream = new MemoryStream(assemblyBytes)) + assembly = AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = assemblyResolver }); + + // skip if already visited + if (visitedAssemblyNames.Contains(assembly.Name.Name)) + yield break; + visitedAssemblyNames.Add(assembly.Name.Name); + + // yield referenced assemblies + foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) + { + FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); + foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) + yield return result; + } + + // yield assembly + yield return new AssemblyParseResult(file, assembly); + } + + /**** + ** Assembly rewriting + ****/ + /// Rewrite the types referenced by an assembly. + /// The assembly to rewrite. + /// Assume the mod is compatible, even if incompatible code is detected. + /// Returns whether the assembly was modified. + /// An incompatible CIL instruction was found while rewriting the assembly. + private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) + { + ModuleDefinition module = assembly.MainModule; + HashSet loggedMessages = new HashSet(); + + // swap assembly references if needed (e.g. XNA => MonoGame) + bool platformChanged = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) + { + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); + } + + // find (and optionally rewrite) incompatible instructions + bool anyRewritten = false; + IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); + foreach (MethodDefinition method in this.GetMethods(module)) + { + // check method definition + foreach (IInstructionRewriter rewriter in rewriters) + { + try + { + if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + anyRewritten = true; + } + } + catch (IncompatibleInstructionException) + { + if (!assumeCompatible) + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); + this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + } + } + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + foreach (Instruction instruction in cil.Body.Instructions.ToArray()) + { + foreach (IInstructionRewriter rewriter in rewriters) + { + try + { + if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + anyRewritten = true; + } + } + catch (IncompatibleInstructionException) + { + if (!assumeCompatible) + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); + this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + } + } + } + } + + return platformChanged || anyRewritten; + } + + /// Get the correct reference to use for compatibility with the current platform. + /// The type reference to rewrite. + private void ChangeTypeScope(TypeReference type) + { + // check skip conditions + if (type == null || type.FullName.StartsWith("System.")) + return; + + // get assembly + Assembly assembly; + if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) + return; + + // replace scope + AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; + type.Scope = assemblyRef; + } + + /// Get all methods in a module. + /// The module to search. + private IEnumerable GetMethods(ModuleDefinition module) + { + return ( + from type in module.GetTypes() + where type.HasMethods + from method in type.Methods + where method.HasBody + select method + ); + } + + /// Log a message for the player or developer the first time it occurs. + /// The monitor through which to log the message. + /// The hash of logged messages. + /// The message to log. + /// The log severity level. + private void LogOnce(IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs new file mode 100644 index 00000000..69c99afe --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -0,0 +1,31 @@ +using System.IO; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata about a parsed assembly definition. + internal class AssemblyParseResult + { + /********* + ** Accessors + *********/ + /// The original assembly file. + public readonly FileInfo File; + + /// The assembly definition. + public readonly AssemblyDefinition Definition; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The original assembly file. + /// The assembly definition. + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) + { + this.File = file; + this.Definition = assembly; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs new file mode 100644 index 00000000..1ac167dc --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -0,0 +1,40 @@ +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for a mod. + internal class ModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string DisplayName { get; } + + /// The mod's full directory path. + public string DirectoryPath { get; } + + /// The mod manifest. + public IManifest Manifest { get; } + + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModCompatibility Compatibility { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's display name. + /// The mod's full directory path. + /// The mod manifest. + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + { + this.DisplayName = displayName; + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Compatibility = compatibility; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModMetadata.cs deleted file mode 100644 index aeb9261a..00000000 --- a/src/StardewModdingAPI/Framework/ModMetadata.cs +++ /dev/null @@ -1,40 +0,0 @@ -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework -{ - /// Metadata for a mod. - internal class ModMetadata - { - /********* - ** Accessors - *********/ - /// The mod's display name. - public string DisplayName { get; } - - /// The mod's full directory path. - public string DirectoryPath { get; } - - /// The mod manifest. - public IManifest Manifest { get; } - - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModCompatibility Compatibility { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's display name. - /// The mod's full directory path. - /// The mod manifest. - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) - { - this.DisplayName = displayName; - this.DirectoryPath = directoryPath; - this.Manifest = manifest; - this.Compatibility = compatibility; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs new file mode 100644 index 00000000..79be2075 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.Models +{ + /// A manifest which describes a mod for SMAPI. + internal class Manifest : IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + [JsonConverter(typeof(SemanticVersionConverter))] + public ISemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + public string MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the method. + public string EntryDll { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Whether the mod uses per-save config files. + [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] + public bool PerSaveConfigs { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + } +} diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index a3169fb3..a65b135c 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -1,6 +1,7 @@ using System; using System.IO; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI { diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a5bd7788..b8d70ad7 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewValley; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 69c167c9..aec32560 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -144,8 +144,8 @@ - - + + @@ -158,7 +158,7 @@ - + @@ -182,7 +182,7 @@ - + @@ -202,7 +202,7 @@ - + -- cgit From 588b42742da93c3807eb3e4d6e6a75d7dda471be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 12 May 2017 21:09:20 -0400 Subject: fix error on Linux/Mac when a mod tries to load content immediately after save is loaded --- release-notes.md | 1 + src/StardewModdingAPI/Framework/SGame.cs | 1191 +++++++++++++++--------------- 2 files changed, 599 insertions(+), 593 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index f48a5e9d..f334a82f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -18,6 +18,7 @@ For players: * SMAPI now remembers if your game crashed and offers help next time you relaunch. * Fixed installer finding redundant game paths on Linux. * Fixed save events not being raised after the first day on Linux/Mac. +* Fixed error on Linux/Mac when a mod tries to load content immediately after the save is loaded. For mod developers: * Added log entries for basic context changes (e.g. loaded save) to simplify troubleshooting. More detailed logging can be enabled by setting `VerboseLogging: true` in `StardewModdingAPI.config.json`. diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f2098dbd..7ee72af2 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -264,7 +264,7 @@ namespace StardewModdingAPI.Framework base.Update(gameTime); return; } - if(this.IsBetweenSaveEvents) + if (this.IsBetweenSaveEvents) { // raise after-save this.IsBetweenSaveEvents = false; @@ -543,7 +543,7 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); } - + /********* ** Update events *********/ @@ -579,6 +579,53 @@ namespace StardewModdingAPI.Framework /// The method called to draw everything to the screen. /// A snapshot of the game timing state. + protected override void Draw(GameTime gameTime) + { + Context.IsInDrawLoop = true; + try + { + this.DrawImpl(gameTime); + this.FailedDraws = 0; + } + catch (Exception ex) + { + // log error + this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); + + // exit if irrecoverable + if (this.FailedDraws >= this.MaxFailedDraws) + { + this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); + return; + } + this.FailedDraws++; + + // abort in known unrecoverable cases + if (Game1.toolSpriteSheet?.IsDisposed == true) + { + this.Monitor.ExitGameImmediately("the game unexpectedly disposed the tool spritesheet, so it crashed trying to draw a tool. This is a known bug in Stardew Valley 1.2.29, and there's no way to recover from it."); + return; + } + + // recover sprite batch + try + { + if (Game1.spriteBatch.IsOpen(SGame.Reflection)) + { + this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); + Game1.spriteBatch.End(); + } + } + catch (Exception innerEx) + { + this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error); + } + } + Context.IsInDrawLoop = false; + } + + /// Replicate the game's draw logic with some changes for SMAPI. + /// A snapshot of the game timing state. /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] @@ -587,694 +634,652 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] - protected override void Draw(GameTime gameTime) + private void DrawImpl(GameTime gameTime) { - Context.IsInDrawLoop = true; - try + if (Game1.debugMode) { - if (Game1.debugMode) + if (SGame._fpsStopwatch.IsRunning) { - if (SGame._fpsStopwatch.IsRunning) - { - float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; - SGame._fpsList.Add(totalSeconds); - while (SGame._fpsList.Count >= 120) - SGame._fpsList.RemoveAt(0); - float num = 0.0f; - foreach (float fps in SGame._fpsList) - num += fps; - SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); - } - SGame._fpsStopwatch.Restart(); + float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; + SGame._fpsList.Add(totalSeconds); + while (SGame._fpsList.Count >= 120) + SGame._fpsList.RemoveAt(0); + float num = 0.0f; + foreach (float fps in SGame._fpsList) + num += fps; + SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); } - else - { - if (SGame._fpsStopwatch.IsRunning) - SGame._fpsStopwatch.Reset(); - SGame._fps = 0.0f; - SGame._fpsList.Clear(); - } - if (SGame._newDayTask != null) + SGame._fpsStopwatch.Restart(); + } + else + { + if (SGame._fpsStopwatch.IsRunning) + SGame._fpsStopwatch.Reset(); + SGame._fps = 0.0f; + SGame._fpsList.Clear(); + } + if (SGame._newDayTask != null) + { + this.GraphicsDevice.Clear(this.bgColor); + //base.Draw(gameTime); + } + else + { + if ((double)Game1.options.zoomLevel != 1.0) + this.GraphicsDevice.SetRenderTarget(this.screenWrapper); + if (this.IsSaving) { this.GraphicsDevice.Clear(this.bgColor); + IClickableMenu activeClickableMenu = Game1.activeClickableMenu; + if (activeClickableMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + try + { + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + catch (Exception ex) + { + this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + activeClickableMenu.exitThisMenu(); + } + Game1.spriteBatch.End(); + } //base.Draw(gameTime); + this.renderScreenBuffer(); } else { - if ((double)Game1.options.zoomLevel != 1.0) - this.GraphicsDevice.SetRenderTarget(this.screenWrapper); - if (this.IsSaving) + this.GraphicsDevice.Clear(this.bgColor); + if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) { - this.GraphicsDevice.Clear(this.bgColor); - IClickableMenu activeClickableMenu = Game1.activeClickableMenu; - if (activeClickableMenu != null) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + try { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - activeClickableMenu.exitThisMenu(); - } + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - //base.Draw(gameTime); - this.renderScreenBuffer(); + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); } - else + else if ((int)Game1.gameMode == 11) + { + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + Game1.spriteBatch.End(); + } + else if (Game1.currentMinigame != null) { - this.GraphicsDevice.Clear(this.bgColor); - if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) + Game1.currentMinigame.draw(Game1.spriteBatch); + if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.End(); + } + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else if (Game1.showingEndOfNightStuff) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.activeClickableMenu != null) + { try { - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); Game1.activeClickableMenu.draw(Game1.spriteBatch); GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); } catch (Exception ex) { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); Game1.activeClickableMenu.exitThisMenu(); } - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); } - else if ((int)Game1.gameMode == 11) + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) { - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - else if (Game1.currentMinigame != null) + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else if ((int)Game1.gameMode == 6) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + string str1 = ""; + for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) + str1 += "."; + string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); + string str3 = str1; + string s = str2 + str3; + string str4 = "..."; + string str5 = str2 + str4; + int widthOfString = SpriteText.getWidthOfString(str5); + int height = 64; + int x = 64; + int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; + SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) { - Game1.currentMinigame.draw(Game1.spriteBatch); - if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); - } - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } - else if (Game1.showingEndOfNightStuff) + if (Game1.overlayMenu == null) + return; + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + else + { + Microsoft.Xna.Framework.Rectangle rectangle; + if ((int)Game1.gameMode == 0) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.activeClickableMenu != null) - { - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); } - else if ((int)Game1.gameMode == 6) + else { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - string str1 = ""; - for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) - str1 += "."; - string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string str3 = str1; - string s = str2 + str3; - string str4 = "..."; - string str5 = str2 + str4; - int widthOfString = SpriteText.getWidthOfString(str5); - int height = 64; - int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; - SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) + if (Game1.drawLighting) { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + this.GraphicsDevice.SetRenderTarget(Game1.lightmap); + this.GraphicsDevice.Clear(Color.White * 0.0f); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight)); + for (int index = 0; index < Game1.currentLightSources.Count; ++index) + { + if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(index).position, (int)((double)Game1.currentLightSources.ElementAt(index).radius * (double)Game1.tileSize * 4.0))) + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); + } Game1.spriteBatch.End(); + this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); } - if (Game1.overlayMenu == null) - return; + if (Game1.bloomDay && Game1.bloom != null) + Game1.bloom.BeginDraw(); + this.GraphicsDevice.Clear(this.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else - { - Microsoft.Xna.Framework.Rectangle rectangle; - if ((int)Game1.gameMode == 0) + GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); + if (Game1.background != null) + Game1.background.draw(Game1.spriteBatch); + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.currentLocation.drawWater(Game1.spriteBatch); + if (Game1.CurrentEvent == null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + foreach (NPC character in Game1.currentLocation.characters) + { + if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + } } else { - if (Game1.drawLighting) - { - this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Color.White * 0.0f); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight)); - for (int index = 0; index < Game1.currentLightSources.Count; ++index) - { - if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(index).position, (int)((double)Game1.currentLightSources.ElementAt(index).radius * (double)Game1.tileSize * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); - } - Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); - } - if (Game1.bloomDay && Game1.bloom != null) - 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); - if (Game1.background != null) - Game1.background.draw(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.currentLocation.drawWater(Game1.spriteBatch); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); - } - } - Microsoft.Xna.Framework.Rectangle bounds; - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) + foreach (NPC actor in Game1.CurrentEvent.actors) { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } - Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.CurrentEvent == null) + } + Microsoft.Xna.Framework.Rectangle bounds; + if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + double x = (double)Game1.shadowTexture.Bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); + int num3 = 0; + double num4 = 0.0; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + } + Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.CurrentEvent == null) + { + foreach (NPC character in Game1.currentLocation.characters) { - foreach (NPC character in Game1.currentLocation.characters) + if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) { - if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale; + int num3 = 0; + double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); } } - else + } + else + { + foreach (NPC actor in Game1.CurrentEvent.actors) { - foreach (NPC actor in Game1.CurrentEvent.actors) + if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) { - if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3)))); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale; + int num3 = 0; + double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); } } - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - rectangle = Game1.shadowTexture.Bounds; - double y = (double)rectangle.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.displayFarmer) - Game1.player.draw(Game1.spriteBatch); - if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0)); - Game1.currentLocation.draw(Game1.spriteBatch); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; - } - if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Name.Equals("Farm")) - this.drawFarmBuildings(); - if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); - foreach (Warp warp in Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U) - { - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - } - if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null) + } + if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num1 = 0.0; + double x = (double)Game1.shadowTexture.Bounds.Center.X; + rectangle = Game1.shadowTexture.Bounds; + double y = (double)rectangle.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); + int num3 = 0; + double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + } + if (Game1.displayFarmer) + Game1.player.draw(Game1.spriteBatch); + if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) + Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); + if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0)); + Game1.currentLocation.draw(Game1.spriteBatch); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; + } + if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Name.Equals("Farm")) + this.drawFarmBuildings(); + if (Game1.tvStation >= 0) + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + if (Game1.panMode) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); + foreach (Warp warp in Game1.currentLocation.warps) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); + } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U) + { + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); + } + if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawPlayerHeldObject(Game1.player); + else if (Game1.displayFarmer && Game1.player.ActiveObject != null) + { + if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) { - if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) + Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); + rectangle = Game1.player.GetBoundingBox(); + Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); + Size size1 = Game1.viewport.Size; + if (layer1.PickTile(mapDisplayLocation1, size1) != null) { - Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); + Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size1 = Game1.viewport.Size; - if (layer1.PickTile(mapDisplayLocation1, size1) != null) - { - Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size2 = Game1.viewport.Size; - if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_127; - } - else + Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); + Size size2 = Game1.viewport.Size; + if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) goto label_127; } - Game1.drawPlayerHeldObject(Game1.player); - } - label_127: - if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) - { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - } - if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) - { - Color color = Color.White; - switch ((int)((double)Game1.toolHold / 600.0) + 2) - { - case 1: - color = Tool.copperColor; - break; - case 2: - color = Tool.steelColor; - break; - case 3: - color = Tool.goldColor; - break; - case 4: - color = Tool.iridiumColor; - break; - } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color); + else + goto label_127; } - if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) - Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize))))) + Game1.drawPlayerHeldObject(Game1.player); + } + label_127: + if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + { + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + } + if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + { + Color color = Color.White; + switch ((int)((double)Game1.toolHold / 600.0) + 2) { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + case 1: + color = Tool.copperColor; + break; + case 2: + color = Tool.steelColor; + break; + case 3: + color = Tool.goldColor; + break; + case 4: + color = Tool.iridiumColor; + break; } - Game1.spriteBatch.End(); - //base.Draw(gameTime); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color); + } + if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) + { + foreach (WeatherDebris weatherDebris in Game1.debrisWeather) + weatherDebris.draw(Game1.spriteBatch); + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); + if (Game1.screenGlow) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); + if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) + Game1.player.CurrentTool.draw(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize))))) + { + for (int index = 0; index < Game1.rainDrops.Length; ++index) + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + } + Game1.spriteBatch.End(); + //base.Draw(gameTime); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + foreach (NPC actor in Game1.currentLocation.currentEvent.actors) { - foreach (NPC actor in Game1.currentLocation.currentEvent.actors) + if (actor.isEmoting) { - if (actor.isEmoting) - { - Vector2 localPosition = actor.getLocalPosition(Game1.viewport); - localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3); - if (actor.age == 2) - localPosition.Y += (float)(Game1.tileSize / 2); - else if (actor.gender == 1) - localPosition.Y += (float)(Game1.tileSize / 6); - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); - } + Vector2 localPosition = actor.getLocalPosition(Game1.viewport); + localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3); + if (actor.age == 2) + localPosition.Y += (float)(Game1.tileSize / 2); + else if (actor.gender == 1) + localPosition.Y += (float)(Game1.tileSize / 6); + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); } } + } + Game1.spriteBatch.End(); + if (Game1.drawLighting) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); + if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); Game1.spriteBatch.End(); - if (Game1.drawLighting) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); - if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.drawGrid) - { - int x1 = -Game1.viewport.X % Game1.tileSize; - float num1 = (float)(-Game1.viewport.Y % Game1.tileSize); - int x2 = x1; - while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); - x2 += Game1.tileSize; - } - float num2 = num1; - while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); - num2 += (float)Game1.tileSize; - } - } - if (Game1.currentBillboard != 0) - this.drawBillboard(); - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.drawGrid) + { + int x1 = -Game1.viewport.X % Game1.tileSize; + float num1 = (float)(-Game1.viewport.Y % Game1.tileSize); + int x2 = x1; + while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) { - GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); - this.drawHUD(); - GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); + x2 += Game1.tileSize; } - 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); - if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) + float num2 = num1; + while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) { - for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) - Game1.hudMessages[i].draw(Game1.spriteBatch, i); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); + num2 += (float)Game1.tileSize; } } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) - this.drawDialogueBox(); - Viewport viewport; - if (Game1.progressBar) + if (Game1.currentBillboard != 0) + this.drawBillboard(); + if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea; - int y1 = rectangle.Bottom - Game1.tileSize * 2; - int dialogueWidth = Game1.dialogueWidth; - int height1 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); - Color lightGray = Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - viewport = Game1.graphics.GraphicsDevice.Viewport; - rectangle = viewport.TitleSafeArea; - int y2 = rectangle.Bottom - Game1.tileSize * 2; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - int height2 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); - Color dimGray = Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); + this.drawHUD(); + GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); } - if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) + 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); + if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Blue * 0.2f; - spriteBatch.Draw(staminaRect, bounds, color); + for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) + Game1.hudMessages[i].draw(Game1.spriteBatch, i); } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) + this.drawDialogueBox(); + Viewport viewport; + if (Game1.progressBar) + { + SpriteBatch spriteBatch1 = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; + rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea; + int y1 = rectangle.Bottom - Game1.tileSize * 2; + int dialogueWidth = Game1.dialogueWidth; + int height1 = Game1.tileSize / 2; + Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); + Color lightGray = Color.LightGray; + spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); + SpriteBatch spriteBatch2 = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; + viewport = Game1.graphics.GraphicsDevice.Viewport; + rectangle = viewport.TitleSafeArea; + int y2 = rectangle.Bottom - Game1.tileSize * 2; + int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); + int height2 = Game1.tileSize / 2; + Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); + Color dimGray = Color.DimGray; + spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + } + if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) + Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Blue * 0.2f; + spriteBatch.Draw(staminaRect, bounds, color); + } + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; + Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } + else if ((double)Game1.flashAlpha > 0.0) + { + if (Game1.options.screenFlash) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D fadeToBlackRect = Game1.fadeToBlackRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); + Color color = Color.White * Math.Min(1f, Game1.flashAlpha); spriteBatch.Draw(fadeToBlackRect, bounds, color); } - else if ((double)Game1.flashAlpha > 0.0) - { - if (Game1.options.screenFlash) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.White * Math.Min(1f, Game1.flashAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - Game1.flashAlpha -= 0.1f; - } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) - this.drawDialogueBox(); - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0); - if (Game1.debugMode) + Game1.flashAlpha -= 0.1f; + } + if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) + this.drawDialogueBox(); + foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) + overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0); + if (Game1.debugMode) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + SpriteFont smallFont = Game1.smallFont; + object[] objArray = new object[10]; + int index1 = 0; + string str1; + if (!Game1.panMode) + str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize); + else + str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize); + objArray[index1] = (object)str1; + int index2 = 1; + string str2 = " mouseTransparency: "; + objArray[index2] = (object)str2; + int index3 = 2; + float cursorTransparency = Game1.mouseCursorTransparency; + objArray[index3] = (object)cursorTransparency; + int index4 = 3; + string str3 = " mousePosition: "; + objArray[index4] = (object)str3; + int index5 = 4; + int mouseX = Game1.getMouseX(); + objArray[index5] = (object)mouseX; + int index6 = 5; + string str4 = ","; + objArray[index6] = (object)str4; + int index7 = 6; + int mouseY = Game1.getMouseY(); + objArray[index7] = (object)mouseY; + int index8 = 7; + string newLine = Environment.NewLine; + objArray[index8] = (object)newLine; + int index9 = 8; + string str5 = "debugOutput: "; + objArray[index9] = (object)str5; + int index10 = 9; + string debugOutput = Game1.debugOutput; + objArray[index10] = (object)debugOutput; + string text = string.Concat(objArray); + Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); + Color red = Color.Red; + double num1 = 0.0; + Vector2 zero = Vector2.Zero; + double num2 = 1.0; + int num3 = 0; + double num4 = 0.99999988079071; + spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4); + } + if (Game1.showKeyHelp) + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + if (Game1.activeClickableMenu != null) + { + try { - SpriteBatch spriteBatch = Game1.spriteBatch; - SpriteFont smallFont = Game1.smallFont; - object[] objArray = new object[10]; - int index1 = 0; - string str1; - if (!Game1.panMode) - str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize); - else - str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize); - objArray[index1] = (object)str1; - int index2 = 1; - string str2 = " mouseTransparency: "; - objArray[index2] = (object)str2; - int index3 = 2; - float cursorTransparency = Game1.mouseCursorTransparency; - objArray[index3] = (object)cursorTransparency; - int index4 = 3; - string str3 = " mousePosition: "; - objArray[index4] = (object)str3; - int index5 = 4; - int mouseX = Game1.getMouseX(); - objArray[index5] = (object)mouseX; - int index6 = 5; - string str4 = ","; - objArray[index6] = (object)str4; - int index7 = 6; - int mouseY = Game1.getMouseY(); - objArray[index7] = (object)mouseY; - int index8 = 7; - string newLine = Environment.NewLine; - objArray[index8] = (object)newLine; - int index9 = 8; - string str5 = "debugOutput: "; - objArray[index9] = (object)str5; - int index10 = 9; - string debugOutput = Game1.debugOutput; - objArray[index10] = (object)debugOutput; - string text = string.Concat(objArray); - Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); - Color red = Color.Red; - double num1 = 0.0; - Vector2 zero = Vector2.Zero; - double num2 = 1.0; - int num3 = 0; - double num4 = 0.99999988079071; - spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4); + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); } - if (Game1.showKeyHelp) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null) + catch (Exception ex) { - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); } - else if (Game1.farmEvent != null) - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + } + else if (Game1.farmEvent != null) + Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + Game1.spriteBatch.End(); + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - - if (GraphicsEvents.HasPostRenderListeners()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); - Game1.spriteBatch.End(); - } - - this.renderScreenBuffer(); } - } - } - - // reset failed draw count - this.FailedDraws = 0; - } - catch (Exception ex) - { - // log error - this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); - // exit if irrecoverable - if (this.FailedDraws >= this.MaxFailedDraws) - { - this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); - return; - } - this.FailedDraws++; - - // abort in known unrecoverable cases - if (Game1.toolSpriteSheet?.IsDisposed == true) - { - this.Monitor.ExitGameImmediately("the game unexpectedly disposed the tool spritesheet, so it crashed trying to draw a tool. This is a known bug in Stardew Valley 1.2.29, and there's no way to recover from it."); - return; - } + if (GraphicsEvents.HasPostRenderListeners()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + Game1.spriteBatch.End(); + } - // recover sprite batch - try - { - if (Game1.spriteBatch.IsOpen(SGame.Reflection)) - { - this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); - Game1.spriteBatch.End(); + this.renderScreenBuffer(); } } - catch (Exception innerEx) - { - this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error); - } } - Context.IsInDrawLoop = false; } /**** -- cgit From 3da27346c6886fff4afb35d7fb46345c92ef1197 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 12 May 2017 01:15:02 -0400 Subject: add basic dependencies to manifest (#285) --- src/StardewModdingAPI/Framework/Models/Manifest.cs | 6 +- .../Framework/Models/ManifestDependency.cs | 23 +++++++ .../Serialisation/ManifestFieldConverter.cs | 72 ++++++++++++++++++++++ .../Serialisation/SemanticVersionConverter.cs | 51 --------------- src/StardewModdingAPI/IManifest.cs | 3 + src/StardewModdingAPI/IManifestDependency.cs | 12 ++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 4 +- 7 files changed, 118 insertions(+), 53 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/ManifestDependency.cs create mode 100644 src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs delete mode 100644 src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs create mode 100644 src/StardewModdingAPI/IManifestDependency.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 79be2075..be781585 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// The mod version. - [JsonConverter(typeof(SemanticVersionConverter))] + [JsonConverter(typeof(ManifestFieldConverter))] public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. @@ -30,6 +30,10 @@ namespace StardewModdingAPI.Framework.Models /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } + /// The other mods that must be loaded before this mod. + [JsonConverter(typeof(ManifestFieldConverter))] + public IManifestDependency[] Dependencies { get; set; } + /// The unique mod ID. public string UniqueID { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs new file mode 100644 index 00000000..2f580c1d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs @@ -0,0 +1,23 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// A mod dependency listed in a mod manifest. + internal class ManifestDependency : IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + public string UniqueID { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique mod ID to require. + public ManifestDependency(string uniqueID) + { + this.UniqueID = uniqueID; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs new file mode 100644 index 00000000..6b5a6aaa --- /dev/null +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Overrides how SMAPI reads and writes and fields. + internal class ManifestFieldConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // semantic version + if (objectType == typeof(ISemanticVersion)) + { + JObject obj = JObject.Load(reader); + int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); + string build = obj.Value(nameof(ISemanticVersion.Build)); + return new SemanticVersion(major, minor, patch, build); + } + + // manifest dependency + if (objectType == typeof(IManifestDependency[])) + { + List result = new List(); + foreach (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); + result.Add(new ManifestDependency(uniqueID)); + } + return result.ToArray(); + } + + // unknown + throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs deleted file mode 100644 index 52ec999e..00000000 --- a/src/StardewModdingAPI/Framework/Serialisation/SemanticVersionConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Overrides how SMAPI reads and writes . - internal class SemanticVersionConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ISemanticVersion); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - JObject obj = JObject.Load(reader); - int major = obj.Value("MajorVersion"); - int minor = obj.Value("MinorVersion"); - int patch = obj.Value("PatchVersion"); - string build = obj.Value("Build"); - return new SemanticVersion(major, minor, patch, build); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 38b83347..9533aadb 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -29,6 +29,9 @@ namespace StardewModdingAPI /// The name of the DLL in the directory that has the method. string EntryDll { get; } + /// The other mods that must be loaded before this mod. + IManifestDependency[] Dependencies { get; } + /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } } diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs new file mode 100644 index 00000000..7bd2e8b6 --- /dev/null +++ b/src/StardewModdingAPI/IManifestDependency.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// A mod dependency listed in a mod manifest. + public interface IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + string UniqueID { get; } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 2a150eb6..86fc8b2b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -133,6 +133,7 @@ + @@ -141,13 +142,14 @@ - + + -- cgit From 66d2b5746ab063b89ca42525a78e217e71d00858 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 17:24:41 -0400 Subject: move mod metadata resolution into its own class (#285) --- .../Framework/ModLoading/ModResolver.cs | 300 +++++++++++++++++++++ src/StardewModdingAPI/Framework/ModRegistry.cs | 28 -- src/StardewModdingAPI/Program.cs | 250 +---------------- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 304 insertions(+), 275 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs new file mode 100644 index 00000000..450fe6bf --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Finds and processes mod metadata. + internal class ModResolver + { + /********* + ** Properties + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Manages deprecation warnings. + private readonly DeprecationManager DeprecationManager; + + /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + private readonly ModCompatibility[] CompatibilityRecords; + + + /********* + ** Public methods + *********/ + public ModResolver(IMonitor monitor, DeprecationManager deprecationManager, IEnumerable compatibilityRecords) + { + this.Monitor = monitor; + this.DeprecationManager = deprecationManager; + this.CompatibilityRecords = compatibilityRecords.ToArray(); + } + + /// Find all mods in the given folder. + /// The root mod path to search. + /// The JSON helper with which to read the manifest file. + /// A list to populate with any deprecation warnings. + public ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList deprecationWarnings) + { + this.Monitor.Log("Finding mods..."); + void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); + + // load mod metadata + List mods = new List(); + foreach (string modRootPath in Directory.GetDirectories(rootPath)) + { + if (this.Monitor.IsExiting) + return new ModMetadata[0]; // exit in progress + + // init metadata + string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); + + // passthrough empty directories + DirectoryInfo directory = new DirectoryInfo(modRootPath); + while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) + directory = directory.GetDirectories().First(); + + // get manifest path + string manifestPath = Path.Combine(directory.FullName, "manifest.json"); + if (!File.Exists(manifestPath)) + { + LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); + continue; + } + + // read manifest + Manifest manifest; + try + { + // read manifest file + string json = File.ReadAllText(manifestPath); + if (string.IsNullOrEmpty(json)) + { + LogSkip(displayName, "its manifest is empty."); + continue; + } + + // parse manifest + manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); + if (manifest == null) + { + LogSkip(displayName, "its manifest is invalid."); + continue; + } + + // validate manifest + if (string.IsNullOrWhiteSpace(manifest.EntryDll)) + { + LogSkip(displayName, "its manifest doesn't set an entry DLL."); + continue; + } + if (string.IsNullOrWhiteSpace(manifest.UniqueID)) + deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); + } + catch (Exception ex) + { + LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); + continue; + } + if (!string.IsNullOrWhiteSpace(manifest.Name)) + displayName = manifest.Name; + + // validate compatibility + ModCompatibility compatibility = this.GetCompatibilityRecord(manifest); + if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) + { + bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); + bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); + + string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + if (hasOfficialUrl) + error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + if (hasUnofficialUrl) + error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + + LogSkip(displayName, error); + } + + // validate SMAPI version + if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) + { + try + { + ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); + if (minVersion.IsNewerThan(Constants.ApiVersion)) + { + LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); + continue; + } + } + catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) + { + LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); + continue; + } + } + + // create per-save directory + if (manifest.PerSaveConfigs) + { + deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); + try + { + string psDir = Path.Combine(directory.FullName, "psconfigs"); + Directory.CreateDirectory(psDir); + if (!Directory.Exists(psDir)) + { + LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); + continue; + } + } + catch (Exception ex) + { + LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); + continue; + } + } + + // validate DLL path + string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); + if (!File.Exists(assemblyPath)) + { + LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); + continue; + } + + // add mod metadata + mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); + } + + return this.HandleModDependencies(mods.ToArray()); + } + + + /********* + ** Private methods + *********/ + /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code. + /// The mod manifest. + /// Returns the incompatibility record if applicable, else null. + private ModCompatibility GetCompatibilityRecord(IManifest manifest) + { + string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + return ( + from mod in this.CompatibilityRecords + where + mod.ID == key + && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) + && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + select mod + ).FirstOrDefault(); + } + + /// Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. + /// The mods to process. + private ModMetadata[] HandleModDependencies(ModMetadata[] mods) + { + this.Monitor.Log("Checking mod dependencies..."); + var unsortedMods = mods.ToList(); + var sortedMods = new Stack(); + var visitedMods = new bool[unsortedMods.Count]; + var currentChain = new List(); + bool success = true; + + for (int index = 0; index < unsortedMods.Count; index++) + { + success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + if (!success) + break; + } + + if (!success) + { + // Failed to sort list, return no mods. + this.Monitor.Log("No mods will be loaded.", LogLevel.Error); + return new ModMetadata[0]; + } + + return sortedMods.Reverse().ToArray(); + } + + /// 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. + /// The index of the mod being processed in the . + /// The mods which have been processed. + /// The list in which to save mods sorted by dependency order. + /// The current change of mod dependencies. + /// The mods remaining to sort. + /// Returns whether the mod can be loaded. + private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) + { + // visit mod + if (visitedMods[modIndex]) + return true; // already sorted + ModMetadata mod = unsortedMods[modIndex]; + visitedMods[modIndex] = true; + + // process dependencies + bool success = true; + if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any()) + { + // validate required dependencies are present + { + string missingMods = null; + foreach (IManifestDependency dependency in mod.Manifest.Dependencies) + { + if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) + missingMods += $"{dependency.UniqueID}, "; + } + if (missingMods != null) + { + this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); + return false; + } + } + + // get mods which should be loaded before this one + ModMetadata[] modsToLoadFirst = + ( + from unsorted in unsortedMods + where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) + select unsorted + ) + .ToArray(); + + // detect circular references + ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); + if (circularReferenceMod != null) + { + this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); + string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; + for (int i = currentChain.Count - 1; i >= 0; i--) + { + chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; + if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; + } + this.Monitor.Log(chain, LogLevel.Error); + return false; + } + currentChain.Add(mod); + + // recursively sort dependencies + foreach (ModMetadata requiredMod in modsToLoadFirst) + { + int index = unsortedMods.IndexOf(requiredMod); + success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + if (!success) + break; + } + } + + // mark mod sorted + sortedMods.Push(mod); + currentChain.Remove(mod); + return success; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index f015b7ba..3899aa3f 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework { @@ -19,21 +18,10 @@ namespace StardewModdingAPI.Framework /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). private readonly IDictionary ModNamesByAssembly = new Dictionary(); - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - private readonly ModCompatibility[] CompatibilityRecords; - /********* ** Public methods *********/ - /// Construct an instance. - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModRegistry(IEnumerable compatibilityRecords) - { - this.CompatibilityRecords = compatibilityRecords.ToArray(); - } - - /**** ** IModRegistry ****/ @@ -125,21 +113,5 @@ namespace StardewModdingAPI.Framework // no known assembly found return null; } - - /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code. - /// The mod manifest. - /// Returns the incompatibility record if applicable, else null. - internal ModCompatibility GetCompatibilityRecord(IManifest manifest) - { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - return ( - from mod in this.CompatibilityRecords - where - mod.ID == key - && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) - && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) - select mod - ).FirstOrDefault(); - } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a86a9540..7b421895 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -269,7 +269,7 @@ namespace StardewModdingAPI this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components - this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); + this.ModRegistry = new ModRegistry(); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); @@ -316,8 +316,8 @@ namespace StardewModdingAPI // load mods JsonHelper jsonHelper = new JsonHelper(); IList deprecationWarnings = new List(); - ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); - mods = this.HandleModDependencies(mods); + ModMetadata[] mods = new ModResolver(this.Monitor, this.DeprecationManager, this.Settings.ModCompatibility) + .FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); // log deprecation warnings together @@ -338,109 +338,6 @@ namespace StardewModdingAPI new Thread(this.RunConsoleLoop).Start(); } - /// Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. - /// The mods to process. - private ModMetadata[] HandleModDependencies(ModMetadata[] mods) - { - this.Monitor.Log("Checking mod dependencies..."); - var unsortedMods = mods.ToList(); - var sortedMods = new Stack(); - var visitedMods = new bool[unsortedMods.Count]; - var currentChain = new List(); - bool success = true; - - for (int index = 0; index < unsortedMods.Count; index++) - { - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); - if (!success) - break; - } - - if (!success) - { - // Failed to sort list, return no mods. - this.Monitor.Log("No mods will be loaded.", LogLevel.Error); - return new ModMetadata[0]; - } - - return sortedMods.Reverse().ToArray(); - } - - /// 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. - /// The index of the mod being processed in the . - /// The mods which have been processed. - /// The list in which to save mods sorted by dependency order. - /// The current change of mod dependencies. - /// The mods remaining to sort. - /// Returns whether the mod can be loaded. - private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) - { - // visit mod - if (visitedMods[modIndex]) - return true; // already sorted - ModMetadata mod = unsortedMods[modIndex]; - visitedMods[modIndex] = true; - - // process dependencies - bool success = true; - if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any()) - { - // validate required dependencies are present - { - string missingMods = null; - foreach (IManifestDependency dependency in mod.Manifest.Dependencies) - { - if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) - missingMods += $"{dependency.UniqueID}, "; - } - if (missingMods != null) - { - this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); - return false; - } - } - - // get mods which should be loaded before this one - ModMetadata[] modsToLoadFirst = - ( - from unsorted in unsortedMods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) - select unsorted - ) - .ToArray(); - - // detect circular references - ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); - if (circularReferenceMod != null) - { - this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); - string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; - for (int i = currentChain.Count - 1; i >= 0; i--) - { - chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; - if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; - } - this.Monitor.Log(chain, LogLevel.Error); - return false; - } - currentChain.Add(mod); - - // recursively sort dependencies - foreach (ModMetadata requiredMod in modsToLoadFirst) - { - int index = unsortedMods.IndexOf(requiredMod); - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); - if (!success) - break; - } - } - - // mark mod sorted - sortedMods.Push(mod); - currentChain.Remove(mod); - return success; - } - /// Run a loop handling console input. [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] private void RunConsoleLoop() @@ -560,147 +457,6 @@ namespace StardewModdingAPI } } - /// Find all mods in the given folder. - /// The root mod path to search. - /// The JSON helper with which to read the manifest file. - /// A list to populate with any deprecation warnings. - private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList deprecationWarnings) - { - this.Monitor.Log("Finding mods..."); - void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); - - // load mod metadata - List mods = new List(); - foreach (string modRootPath in Directory.GetDirectories(rootPath)) - { - if (this.Monitor.IsExiting) - return new ModMetadata[0]; // exit in progress - - // init metadata - string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); - - // passthrough empty directories - DirectoryInfo directory = new DirectoryInfo(modRootPath); - while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) - directory = directory.GetDirectories().First(); - - // get manifest path - string manifestPath = Path.Combine(directory.FullName, "manifest.json"); - if (!File.Exists(manifestPath)) - { - LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); - continue; - } - - // read manifest - Manifest manifest; - try - { - // read manifest file - string json = File.ReadAllText(manifestPath); - if (string.IsNullOrEmpty(json)) - { - LogSkip(displayName, "its manifest is empty."); - continue; - } - - // parse manifest - manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); - if (manifest == null) - { - LogSkip(displayName, "its manifest is invalid."); - continue; - } - - // validate manifest - if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - { - LogSkip(displayName, "its manifest doesn't set an entry DLL."); - continue; - } - if (string.IsNullOrWhiteSpace(manifest.UniqueID)) - deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); - } - catch (Exception ex) - { - LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); - continue; - } - if (!string.IsNullOrWhiteSpace(manifest.Name)) - displayName = manifest.Name; - - // validate compatibility - ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest); - if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) - { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); - - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; - if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; - if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - - LogSkip(displayName, error); - } - - // validate SMAPI version - if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) - { - try - { - ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); - if (minVersion.IsNewerThan(Constants.ApiVersion)) - { - LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); - continue; - } - } - catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) - { - LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); - continue; - } - } - - // create per-save directory - if (manifest.PerSaveConfigs) - { - deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); - try - { - string psDir = Path.Combine(directory.FullName, "psconfigs"); - Directory.CreateDirectory(psDir); - if (!Directory.Exists(psDir)) - { - LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); - continue; - } - } - catch (Exception ex) - { - LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); - continue; - } - } - - // validate DLL path - string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); - if (!File.Exists(assemblyPath)) - { - LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); - continue; - } - - // add mod metadata - mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); - } - - return mods.ToArray(); - } - /// Load and hook up the given mods. /// The mods to load. /// The JSON helper with which to read mods' JSON files. diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 86fc8b2b..2424f438 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -121,6 +121,7 @@ + -- cgit From 63edebaef1019ce103f5a86d55e1d1c4eb8d371c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 18:20:09 -0400 Subject: decouple mod metadata resolution from main SMAPI logic (#285) This makes the logic more self-contained for eventual unit testing, and makes failed mods available during dependency resolution so we can make errors more relevant. --- .../Framework/ModLoading/ModMetadata.cs | 34 +++ .../Framework/ModLoading/ModResolver.cs | 267 ++++++++++----------- src/StardewModdingAPI/Program.cs | 51 +++- src/StardewModdingAPI/SemanticVersion.cs | 17 ++ 4 files changed, 219 insertions(+), 150 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs index 1ac167dc..72c4692b 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -20,6 +20,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. public ModCompatibility Compatibility { get; } + /// The metadata resolution status. + public ModMetadataStatus Status { get; set; } + + /// The reason the metadata is invalid, if any. + public string Error { get; set; } + /********* ** Public methods @@ -30,11 +36,39 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod manifest. /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + : this(displayName, directoryPath, manifest, compatibility, ModMetadataStatus.Found, null) + { + this.DisplayName = displayName; + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Compatibility = compatibility; + } + + /// Construct an instance. + /// The mod's display name. + /// The mod's full directory path. + /// The mod manifest. + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + /// The metadata resolution status. + /// The reason the metadata is invalid, if any. + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility, ModMetadataStatus status, string error) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; this.Manifest = manifest; this.Compatibility = compatibility; + this.Status = status; + this.Error = error; } } + + /// Indicates the status of a mod's metadata resolution. + internal enum ModMetadataStatus + { + /// The mod has been found, but hasn't been processed yet. + Found, + + /// The mod cannot be loaded. + Failed + } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 450fe6bf..30c38aca 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -13,12 +13,6 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Properties *********/ - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// Manages deprecation warnings. - private readonly DeprecationManager DeprecationManager; - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. private readonly ModCompatibility[] CompatibilityRecords; @@ -26,78 +20,43 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Public methods *********/ - public ModResolver(IMonitor monitor, DeprecationManager deprecationManager, IEnumerable compatibilityRecords) + /// Construct an instance. + /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModResolver(IEnumerable compatibilityRecords) { - this.Monitor = monitor; - this.DeprecationManager = deprecationManager; this.CompatibilityRecords = compatibilityRecords.ToArray(); } + /// Read mod metadata from the given folder in dependency order. + /// The root path to search for mods. + /// The JSON helper with which to read manifests. + public IEnumerable GetMods(string rootPath, JsonHelper jsonHelper) + { + ModMetadata[] mods = this.GetDataFromFolder(rootPath, jsonHelper).ToArray(); + mods = this.ProcessDependencies(mods.ToArray()); + return mods; + } + + + /********* + ** Private methods + *********/ /// Find all mods in the given folder. /// The root mod path to search. /// The JSON helper with which to read the manifest file. - /// A list to populate with any deprecation warnings. - public ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList deprecationWarnings) + private IEnumerable GetDataFromFolder(string rootPath, JsonHelper jsonHelper) { - this.Monitor.Log("Finding mods..."); - void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level); - // load mod metadata - List mods = new List(); - foreach (string modRootPath in Directory.GetDirectories(rootPath)) + foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { - if (this.Monitor.IsExiting) - return new ModMetadata[0]; // exit in progress - - // init metadata - string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\'); - - // passthrough empty directories - DirectoryInfo directory = new DirectoryInfo(modRootPath); - while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) - directory = directory.GetDirectories().First(); - - // get manifest path - string manifestPath = Path.Combine(directory.FullName, "manifest.json"); - if (!File.Exists(manifestPath)) - { - LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn); - continue; - } + string displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); // read manifest Manifest manifest; - try - { - // read manifest file - string json = File.ReadAllText(manifestPath); - if (string.IsNullOrEmpty(json)) - { - LogSkip(displayName, "its manifest is empty."); - continue; - } - - // parse manifest - manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); - if (manifest == null) - { - LogSkip(displayName, "its manifest is invalid."); - continue; - } - - // validate manifest - if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - { - LogSkip(displayName, "its manifest doesn't set an entry DLL."); - continue; - } - if (string.IsNullOrWhiteSpace(manifest.UniqueID)) - deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); - } - catch (Exception ex) { - LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}"); - continue; + string manifestPath = Path.Combine(modDir.FullName, "manifest.json"); + if (!this.TryReadManifest(manifestPath, jsonHelper, out manifest, out string error)) + yield return new ModMetadata(displayName, modDir.FullName, null, null, ModMetadataStatus.Failed, error); } if (!string.IsNullOrWhiteSpace(manifest.Name)) displayName = manifest.Name; @@ -116,89 +75,35 @@ namespace StardewModdingAPI.Framework.ModLoading if (hasUnofficialUrl) error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - LogSkip(displayName, error); + yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, error); } // validate SMAPI version if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) { - try - { - ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); - if (minVersion.IsNewerThan(Constants.ApiVersion)) - { - LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); - continue; - } - } - catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) - { - LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); - continue; - } - } - - // create per-save directory - if (manifest.PerSaveConfigs) - { - deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); - try - { - string psDir = Path.Combine(directory.FullName, "psconfigs"); - Directory.CreateDirectory(psDir); - if (!Directory.Exists(psDir)) - { - LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); - continue; - } - } - catch (Exception ex) - { - LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); - continue; - } + if (!SemanticVersion.TryParse(manifest.MinimumApiVersion, out ISemanticVersion minVersion)) + yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); + if (minVersion.IsNewerThan(Constants.ApiVersion)) + yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); } // validate DLL path - string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); + string assemblyPath = Path.Combine(modDir.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { - LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist."); + yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"its DLL '{manifest.EntryDll}' doesn't exist."); continue; } // add mod metadata - mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility)); + yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility); } - - return this.HandleModDependencies(mods.ToArray()); - } - - - /********* - ** Private methods - *********/ - /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code. - /// The mod manifest. - /// Returns the incompatibility record if applicable, else null. - private ModCompatibility GetCompatibilityRecord(IManifest manifest) - { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - return ( - from mod in this.CompatibilityRecords - where - mod.ID == key - && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) - && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) - select mod - ).FirstOrDefault(); } /// Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. /// The mods to process. - private ModMetadata[] HandleModDependencies(ModMetadata[] mods) + private ModMetadata[] ProcessDependencies(ModMetadata[] mods) { - this.Monitor.Log("Checking mod dependencies..."); var unsortedMods = mods.ToList(); var sortedMods = new Stack(); var visitedMods = new bool[unsortedMods.Count]; @@ -207,17 +112,16 @@ namespace StardewModdingAPI.Framework.ModLoading for (int index = 0; index < unsortedMods.Count; index++) { - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + if (unsortedMods[index].Status == ModMetadataStatus.Failed) + continue; + + success = this.ProcessDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); if (!success) break; } if (!success) - { - // Failed to sort list, return no mods. - this.Monitor.Log("No mods will be loaded.", LogLevel.Error); return new ModMetadata[0]; - } return sortedMods.Reverse().ToArray(); } @@ -229,14 +133,18 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current change of mod dependencies. /// The mods remaining to sort. /// Returns whether the mod can be loaded. - private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) + private bool ProcessDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) { // visit mod if (visitedMods[modIndex]) return true; // already sorted - ModMetadata mod = unsortedMods[modIndex]; visitedMods[modIndex] = true; + // mod already failed + ModMetadata mod = unsortedMods[modIndex]; + if (mod.Status == ModMetadataStatus.Failed) + return false; + // process dependencies bool success = true; if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any()) @@ -251,7 +159,8 @@ namespace StardewModdingAPI.Framework.ModLoading } if (missingMods != null) { - this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error); + mod.Status = ModMetadataStatus.Failed; + mod.Error = $"it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)})."; return false; } } @@ -269,14 +178,8 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); if (circularReferenceMod != null) { - this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error); - string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}"; - for (int i = currentChain.Count - 1; i >= 0; i--) - { - chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain; - if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break; - } - this.Monitor.Log(chain, LogLevel.Error); + mod.Status = ModMetadataStatus.Failed; + mod.Error = $"its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName})."; return false; } currentChain.Add(mod); @@ -285,7 +188,7 @@ namespace StardewModdingAPI.Framework.ModLoading foreach (ModMetadata requiredMod in modsToLoadFirst) { int index = unsortedMods.IndexOf(requiredMod); - success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + success = this.ProcessDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); if (!success) break; } @@ -296,5 +199,81 @@ namespace StardewModdingAPI.Framework.ModLoading currentChain.Remove(mod); return success; } + + /// Get all mod folders in a root folder, passing through empty folders as needed. + /// The root folder path to search. + private IEnumerable GetModFolders(string rootPath) + { + foreach (string modRootPath in Directory.GetDirectories(rootPath)) + { + DirectoryInfo directory = new DirectoryInfo(modRootPath); + + // if a folder only contains another folder, check the inner folder instead + while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) + directory = directory.GetDirectories().First(); + + yield return directory; + } + } + + /// Read a manifest file if it's valid, else set a relevant error phrase. + /// The absolute path to the manifest file. + /// The JSON helper with which to read the manifest file. + /// The loaded manifest, if reading succeeded. + /// The read error, if reading failed. + /// Returns whether the manifest was read successfully. + private bool TryReadManifest(string path, JsonHelper jsonHelper, out Manifest manifest, out string errorPhrase) + { + try + { + // validate path + if (!File.Exists(path)) + { + manifest = null; + errorPhrase = "it doesn't have a manifest."; + return false; + } + + // parse manifest + manifest = jsonHelper.ReadJsonFile(path); + if (manifest == null) + { + errorPhrase = "its manifest is invalid."; + return false; + } + + // validate manifest + if (string.IsNullOrWhiteSpace(manifest.EntryDll)) + { + errorPhrase = "its manifest doesn't set an entry DLL."; + return false; + } + + errorPhrase = null; + return true; + } + catch (Exception ex) + { + manifest = null; + errorPhrase = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; + return false; + } + } + + /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code. + /// The mod manifest. + /// Returns the incompatibility record if applicable, else null. + private ModCompatibility GetCompatibilityRecord(IManifest manifest) + { + string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + return ( + from mod in this.CompatibilityRecords + where + mod.ID == key + && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) + && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + select mod + ).FirstOrDefault(); + } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 7b421895..c8840538 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -313,14 +313,45 @@ namespace StardewModdingAPI // load mods int modsLoaded; { - // load mods + // get mod metadata (in dependency order) + this.Monitor.Log("Loading mod metadata..."); JsonHelper jsonHelper = new JsonHelper(); + ModMetadata[] mods = new ModResolver(this.Settings.ModCompatibility) + .GetMods(Constants.ModPath, new JsonHelper()) + .ToArray(); + + // check for deprecated metadata IList deprecationWarnings = new List(); - ModMetadata[] mods = new ModResolver(this.Monitor, this.DeprecationManager, this.Settings.ModCompatibility) - .FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings); - modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); + foreach (ModMetadata mod in mods) + { + // missing unique ID + if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) + deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} doesn't have specify a {nameof(IManifest.UniqueID)} field in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); - // log deprecation warnings together + // per-save directories + if ((mod.Manifest as Manifest)?.PerSaveConfigs == true) + { + deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.DisplayName, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); + try + { + string psDir = Path.Combine(mod.DirectoryPath, "psconfigs"); + Directory.CreateDirectory(psDir); + if (!Directory.Exists(psDir)) + { + mod.Status = ModMetadataStatus.Failed; + mod.Error = "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."; + } + } + catch (Exception ex) + { + mod.Status = ModMetadataStatus.Failed; + mod.Error = $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"; + } + } + } + + // load mods + modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); foreach (Action warning in deprecationWarnings) warning(); } @@ -474,9 +505,17 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); foreach (ModMetadata metadata in mods) { + // validate status + if (metadata.Status == ModMetadataStatus.Failed) + { + LogSkip(metadata, metadata.Error); + continue; + } + + // get basic info IManifest manifest = metadata.Manifest; string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll); - + // preprocess & load mod assembly Assembly modAssembly; try diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs index db25dc11..a2adb657 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -182,6 +182,23 @@ namespace StardewModdingAPI return result; } + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out ISemanticVersion parsed) + { + try + { + parsed = new SemanticVersion(version); + return true; + } + catch + { + parsed = null; + return false; + } + } /********* ** Private methods -- cgit From 9b6c0d1021b07ec04b589f1bd0eb69e36082b600 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 18:58:19 -0400 Subject: decouple reading manifest files from validating metadata (#285) --- .../Framework/ModLoading/ModMetadata.cs | 15 +- .../Framework/ModLoading/ModResolver.cs | 219 +++++++++------------ src/StardewModdingAPI/Program.cs | 19 +- 3 files changed, 109 insertions(+), 144 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs index 72c4692b..7be85a83 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -36,7 +36,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod manifest. /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) - : this(displayName, directoryPath, manifest, compatibility, ModMetadataStatus.Found, null) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; @@ -44,21 +43,15 @@ namespace StardewModdingAPI.Framework.ModLoading this.Compatibility = compatibility; } - /// Construct an instance. - /// The mod's display name. - /// The mod's full directory path. - /// The mod manifest. - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + /// Set the mod status. /// The metadata resolution status. /// The reason the metadata is invalid, if any. - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility, ModMetadataStatus status, string error) + /// Return the instance for chaining. + public ModMetadata SetStatus(ModMetadataStatus status, string error = null) { - this.DisplayName = displayName; - this.DirectoryPath = directoryPath; - this.Manifest = manifest; - this.Compatibility = compatibility; this.Status = status; this.Error = error; + return this; } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 30c38aca..829575af 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -10,99 +10,124 @@ namespace StardewModdingAPI.Framework.ModLoading /// Finds and processes mod metadata. internal class ModResolver { - /********* - ** Properties - *********/ - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - private readonly ModCompatibility[] CompatibilityRecords; - - /********* ** Public methods *********/ - /// Construct an instance. - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModResolver(IEnumerable compatibilityRecords) - { - this.CompatibilityRecords = compatibilityRecords.ToArray(); - } - - /// Read mod metadata from the given folder in dependency order. + /// Get manifest metadata for each folder in the given root path. /// The root path to search for mods. /// The JSON helper with which to read manifests. - public IEnumerable GetMods(string rootPath, JsonHelper jsonHelper) - { - ModMetadata[] mods = this.GetDataFromFolder(rootPath, jsonHelper).ToArray(); - mods = this.ProcessDependencies(mods.ToArray()); - return mods; - } - - - /********* - ** Private methods - *********/ - /// Find all mods in the given folder. - /// The root mod path to search. - /// The JSON helper with which to read the manifest file. - private IEnumerable GetDataFromFolder(string rootPath, JsonHelper jsonHelper) + /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + /// Returns the manifests by relative folder. + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) { - // load mod metadata + compatibilityRecords = compatibilityRecords.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { - string displayName = modDir.FullName.Replace(rootPath, "").Trim('/', '\\'); + // read file + Manifest manifest = null; + string path = Path.Combine(modDir.FullName, "manifest.json"); + string error = null; + try + { + // read manifest + manifest = jsonHelper.ReadJsonFile(path); - // read manifest - Manifest manifest; + // 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 (Exception ex) { - string manifestPath = Path.Combine(modDir.FullName, "manifest.json"); - if (!this.TryReadManifest(manifestPath, jsonHelper, out manifest, out string error)) - yield return new ModMetadata(displayName, modDir.FullName, null, null, ModMetadataStatus.Failed, error); + error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - if (!string.IsNullOrWhiteSpace(manifest.Name)) - displayName = manifest.Name; - // validate compatibility - ModCompatibility compatibility = this.GetCompatibilityRecord(manifest); - if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) + // get compatibility record + ModCompatibility compatibility = null; + if(manifest != null) { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); + string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + compatibility = ( + from mod in compatibilityRecords + where + mod.ID == key + && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) + && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + select mod + ).FirstOrDefault(); + } + // 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, compatibility).SetStatus(status, error); + } + } - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; - if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; - if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + /// Validate manifest metadata. + /// The mod manifests to validate. + public void ValidateManifests(IEnumerable mods) + { + foreach (ModMetadata mod in mods) + { + // skip if already failed + if (mod.Status == ModMetadataStatus.Failed) + continue; - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, error); + // validate compatibility + { + ModCompatibility compatibility = mod.Compatibility; + if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) + { + bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl); + bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl); + + string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + if (hasOfficialUrl) + error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + if (hasUnofficialUrl) + error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + + mod.SetStatus(ModMetadataStatus.Failed, error); + continue; + } } // validate SMAPI version - if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion)) + if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion)) { - if (!SemanticVersion.TryParse(manifest.MinimumApiVersion, out ISemanticVersion minVersion)) - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); + if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); + continue; + } if (minVersion.IsNewerThan(Constants.ApiVersion)) - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); + { + mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); + continue; + } } // validate DLL path - string assemblyPath = Path.Combine(modDir.FullName, manifest.EntryDll); + string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) - { - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility, ModMetadataStatus.Failed, $"its DLL '{manifest.EntryDll}' doesn't exist."); - continue; - } - - // add mod metadata - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility); + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); } } - /// Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. + /// Sort the given mods by the order they should be loaded. /// The mods to process. - private ModMetadata[] ProcessDependencies(ModMetadata[] mods) + public IEnumerable ProcessDependencies(IEnumerable mods) { var unsortedMods = mods.ToList(); var sortedMods = new Stack(); @@ -126,6 +151,10 @@ namespace StardewModdingAPI.Framework.ModLoading return sortedMods.Reverse().ToArray(); } + + /********* + ** Private methods + *********/ /// 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. /// The index of the mod being processed in the . /// The mods which have been processed. @@ -215,65 +244,5 @@ namespace StardewModdingAPI.Framework.ModLoading yield return directory; } } - - /// Read a manifest file if it's valid, else set a relevant error phrase. - /// The absolute path to the manifest file. - /// The JSON helper with which to read the manifest file. - /// The loaded manifest, if reading succeeded. - /// The read error, if reading failed. - /// Returns whether the manifest was read successfully. - private bool TryReadManifest(string path, JsonHelper jsonHelper, out Manifest manifest, out string errorPhrase) - { - try - { - // validate path - if (!File.Exists(path)) - { - manifest = null; - errorPhrase = "it doesn't have a manifest."; - return false; - } - - // parse manifest - manifest = jsonHelper.ReadJsonFile(path); - if (manifest == null) - { - errorPhrase = "its manifest is invalid."; - return false; - } - - // validate manifest - if (string.IsNullOrWhiteSpace(manifest.EntryDll)) - { - errorPhrase = "its manifest doesn't set an entry DLL."; - return false; - } - - errorPhrase = null; - return true; - } - catch (Exception ex) - { - manifest = null; - errorPhrase = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; - return false; - } - } - - /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code. - /// The mod manifest. - /// Returns the incompatibility record if applicable, else null. - private ModCompatibility GetCompatibilityRecord(IManifest manifest) - { - string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - return ( - from mod in this.CompatibilityRecords - where - mod.ID == key - && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) - && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) - select mod - ).FirstOrDefault(); - } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index c8840538..74a9ff8e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -313,12 +313,12 @@ namespace StardewModdingAPI // load mods int modsLoaded; { - // get mod metadata (in dependency order) this.Monitor.Log("Loading mod metadata..."); - JsonHelper jsonHelper = new JsonHelper(); - ModMetadata[] mods = new ModResolver(this.Settings.ModCompatibility) - .GetMods(Constants.ModPath, new JsonHelper()) - .ToArray(); + ModResolver resolver = new ModResolver(); + + // load manifests + ModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray(); + resolver.ValidateManifests(mods); // check for deprecated metadata IList deprecationWarnings = new List(); @@ -326,7 +326,7 @@ namespace StardewModdingAPI { // missing unique ID if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) - deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} doesn't have specify a {nameof(IManifest.UniqueID)} field in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); + deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} doesn't have specify a {nameof(IManifest.UniqueID)} field in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); // per-save directories if ((mod.Manifest as Manifest)?.PerSaveConfigs == true) @@ -350,8 +350,11 @@ namespace StardewModdingAPI } } + // process dependencies + mods = resolver.ProcessDependencies(mods).ToArray(); + // load mods - modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings); + modsLoaded = this.LoadMods(mods, new JsonHelper(), (SContentManager)Game1.content, deprecationWarnings); foreach (Action warning in deprecationWarnings) warning(); } @@ -515,7 +518,7 @@ namespace StardewModdingAPI // get basic info IManifest manifest = metadata.Manifest; string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll); - + // preprocess & load mod assembly Assembly modAssembly; try -- cgit From 7f368aa8896baa551aa156a8e67e9dd16416022d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 20:41:00 -0400 Subject: enforce metadata.SetStatus() instead of setting properties directly (#285) --- src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs | 4 ++-- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 6 ++---- src/StardewModdingAPI/Program.cs | 8 ++------ 3 files changed, 6 insertions(+), 12 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs index 7be85a83..5ec2d4e0 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ModLoading public ModCompatibility Compatibility { get; } /// The metadata resolution status. - public ModMetadataStatus Status { get; set; } + public ModMetadataStatus Status { get; private set; } /// The reason the metadata is invalid, if any. - public string Error { get; set; } + public string Error { get; private set; } /********* diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 829575af..9b26e8b0 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -188,8 +188,7 @@ namespace StardewModdingAPI.Framework.ModLoading } if (missingMods != null) { - mod.Status = ModMetadataStatus.Failed; - mod.Error = $"it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)})."; + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)})."); return false; } } @@ -207,8 +206,7 @@ namespace StardewModdingAPI.Framework.ModLoading ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); if (circularReferenceMod != null) { - mod.Status = ModMetadataStatus.Failed; - mod.Error = $"its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName})."; + mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName})."); return false; } currentChain.Add(mod); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 74a9ff8e..37e1e000 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -337,15 +337,11 @@ namespace StardewModdingAPI string psDir = Path.Combine(mod.DirectoryPath, "psconfigs"); Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) - { - mod.Status = ModMetadataStatus.Failed; - mod.Error = "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."; - } + mod.SetStatus(ModMetadataStatus.Failed, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); } catch (Exception ex) { - mod.Status = ModMetadataStatus.Failed; - mod.Error = $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"; + mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); } } } -- cgit From 53547a8ca3a5cba45bd0a5a478d0f40daa282888 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 21:36:50 -0400 Subject: pass API version into mod metadata validation to simplify unit testing (#285) --- .../Framework/ModLoading/IModMetadata.cs | 39 ++++++++++++++++++++++ .../Framework/ModLoading/ModMetadata.cs | 14 ++------ .../Framework/ModLoading/ModMetadataStatus.cs | 12 +++++++ .../Framework/ModLoading/ModResolver.cs | 22 ++++++------ src/StardewModdingAPI/Program.cs | 12 +++---- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 ++ 6 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs new file mode 100644 index 00000000..3771ffdd --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/IModMetadata.cs @@ -0,0 +1,39 @@ +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for a mod. + internal interface IModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + string DisplayName { get; } + + /// The mod's full directory path. + string DirectoryPath { get; } + + /// The mod manifest. + IManifest Manifest { get; } + + /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + ModCompatibility Compatibility { get; } + + /// The metadata resolution status. + ModMetadataStatus Status { get; } + + /// The reason the metadata is invalid, if any. + string Error { get; } + + + /********* + ** Public methods + *********/ + /// Set the mod status. + /// The metadata resolution status. + /// The reason the metadata is invalid, if any. + /// Return the instance for chaining. + IModMetadata SetStatus(ModMetadataStatus status, string error = null); + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs index 5ec2d4e0..7b25e090 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI.Framework.ModLoading { /// Metadata for a mod. - internal class ModMetadata + internal class ModMetadata : IModMetadata { /********* ** Accessors @@ -47,21 +47,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// The metadata resolution status. /// The reason the metadata is invalid, if any. /// Return the instance for chaining. - public ModMetadata SetStatus(ModMetadataStatus status, string error = null) + public IModMetadata SetStatus(ModMetadataStatus status, string error = null) { this.Status = status; this.Error = error; return this; } } - - /// Indicates the status of a mod's metadata resolution. - internal enum ModMetadataStatus - { - /// The mod has been found, but hasn't been processed yet. - Found, - - /// The mod cannot be loaded. - Failed - } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs new file mode 100644 index 00000000..1b2b0b55 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates the status of a mod's metadata resolution. + internal enum ModMetadataStatus + { + /// The mod has been found, but hasn't been processed yet. + Found, + + /// The mod cannot be loaded. + Failed + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 9b26e8b0..a3d4ce3e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The JSON helper with which to read manifests. /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) { compatibilityRecords = compatibilityRecords.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) @@ -75,9 +75,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// Validate manifest metadata. /// The mod manifests to validate. - public void ValidateManifests(IEnumerable mods) + public void ValidateManifests(IEnumerable mods) { - foreach (ModMetadata mod in mods) + foreach (IModMetadata mod in mods) { // skip if already failed if (mod.Status == ModMetadataStatus.Failed) @@ -127,12 +127,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// Sort the given mods by the order they should be loaded. /// The mods to process. - public IEnumerable ProcessDependencies(IEnumerable mods) + public IEnumerable ProcessDependencies(IEnumerable mods) { var unsortedMods = mods.ToList(); - var sortedMods = new Stack(); + var sortedMods = new Stack(); var visitedMods = new bool[unsortedMods.Count]; - var currentChain = new List(); + var currentChain = new List(); bool success = true; for (int index = 0; index < unsortedMods.Count; index++) @@ -162,7 +162,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current change of mod dependencies. /// The mods remaining to sort. /// Returns whether the mod can be loaded. - private bool ProcessDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) + private bool ProcessDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) { // visit mod if (visitedMods[modIndex]) @@ -170,7 +170,7 @@ namespace StardewModdingAPI.Framework.ModLoading visitedMods[modIndex] = true; // mod already failed - ModMetadata mod = unsortedMods[modIndex]; + IModMetadata mod = unsortedMods[modIndex]; if (mod.Status == ModMetadataStatus.Failed) return false; @@ -194,7 +194,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // get mods which should be loaded before this one - ModMetadata[] modsToLoadFirst = + IModMetadata[] modsToLoadFirst = ( from unsorted in unsortedMods where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) @@ -203,7 +203,7 @@ namespace StardewModdingAPI.Framework.ModLoading .ToArray(); // detect circular references - ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); + IModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); if (circularReferenceMod != null) { mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName})."); @@ -212,7 +212,7 @@ namespace StardewModdingAPI.Framework.ModLoading currentChain.Add(mod); // recursively sort dependencies - foreach (ModMetadata requiredMod in modsToLoadFirst) + foreach (IModMetadata requiredMod in modsToLoadFirst) { int index = unsortedMods.IndexOf(requiredMod); success = this.ProcessDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 37e1e000..9ccb4ddc 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -317,12 +317,12 @@ namespace StardewModdingAPI ModResolver resolver = new ModResolver(); // load manifests - ModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray(); resolver.ValidateManifests(mods); // check for deprecated metadata IList deprecationWarnings = new List(); - foreach (ModMetadata mod in mods) + foreach (IModMetadata mod in mods) { // missing unique ID if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) @@ -428,7 +428,7 @@ namespace StardewModdingAPI string[] fields = entry.Value.Split('/'); if (fields.Length < SObject.objectInfoDescriptionIndex + 1) { - LogIssue(entry.Key, $"too few fields for an object"); + LogIssue(entry.Key, "too few fields for an object"); issuesFound = true; continue; } @@ -493,16 +493,16 @@ namespace StardewModdingAPI /// The content manager to use for mod content. /// A list to populate with any deprecation warnings. /// Returns the number of mods successfully loaded. - private int LoadMods(ModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) + private int LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) { this.Monitor.Log("Loading mods..."); - void LogSkip(ModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level); + void LogSkip(IModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level); // load mod assemblies int modsLoaded = 0; AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); - foreach (ModMetadata metadata in mods) + foreach (IModMetadata metadata in mods) { // validate status if (metadata.Status == ModMetadataStatus.Failed) diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 2424f438..a7362153 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -121,6 +121,8 @@ + + -- cgit From f03b300b3fc4bcc9844e77e810dcf352a34b9232 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 May 2017 21:38:04 -0400 Subject: pass SMAPI version into metadata validation to simplify unit tests (#285) --- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 7 ++++--- src/StardewModdingAPI/Program.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index a3d4ce3e..e3f4fc12 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -75,7 +75,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// Validate manifest metadata. /// The mod manifests to validate. - public void ValidateManifests(IEnumerable mods) + /// The current SMAPI version. + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion) { foreach (IModMetadata mod in mods) { @@ -108,10 +109,10 @@ namespace StardewModdingAPI.Framework.ModLoading { if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion)) { - mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}."); + mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {apiVersion}."); continue; } - if (minVersion.IsNewerThan(Constants.ApiVersion)) + if (minVersion.IsNewerThan(apiVersion)) { mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 9ccb4ddc..743de050 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -318,7 +318,7 @@ namespace StardewModdingAPI // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray(); - resolver.ValidateManifests(mods); + resolver.ValidateManifests(mods, Constants.ApiVersion); // check for deprecated metadata IList deprecationWarnings = new List(); -- cgit From 07aadf36126bfadd5df624ccf810828adf679788 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 May 2017 18:17:34 -0400 Subject: replace mod indexes with references in dependency-sorting logic (#285) --- .../Framework/ModLoading/ModResolver.cs | 24 ++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index e3f4fc12..8efe57d9 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.ModLoading // get compatibility record ModCompatibility compatibility = null; - if(manifest != null) + if (manifest != null) { string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; compatibility = ( @@ -132,16 +132,16 @@ namespace StardewModdingAPI.Framework.ModLoading { var unsortedMods = mods.ToList(); var sortedMods = new Stack(); - var visitedMods = new bool[unsortedMods.Count]; + var visitedMods = new HashSet(); var currentChain = new List(); bool success = true; - for (int index = 0; index < unsortedMods.Count; index++) + foreach (IModMetadata mod in unsortedMods) { - if (unsortedMods[index].Status == ModMetadataStatus.Failed) + if (mod.Status == ModMetadataStatus.Failed) continue; - success = this.ProcessDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + success = this.ProcessDependencies(mod, visitedMods, sortedMods, currentChain, unsortedMods); if (!success) break; } @@ -157,21 +157,20 @@ namespace StardewModdingAPI.Framework.ModLoading ** Private methods *********/ /// 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. - /// The index of the mod being processed in the . - /// The mods which have been processed. + /// The mod whose dependencies to process. + /// The mods which have been visited. /// The list in which to save mods sorted by dependency order. /// The current change of mod dependencies. /// The mods remaining to sort. /// Returns whether the mod can be loaded. - private bool ProcessDependencies(int modIndex, bool[] visitedMods, Stack sortedMods, List currentChain, List unsortedMods) + private bool ProcessDependencies(IModMetadata mod, HashSet visited, Stack sortedMods, List currentChain, List unsortedMods) { // visit mod - if (visitedMods[modIndex]) + if (visited.Contains(mod)) return true; // already sorted - visitedMods[modIndex] = true; + visited.Add(mod); // mod already failed - IModMetadata mod = unsortedMods[modIndex]; if (mod.Status == ModMetadataStatus.Failed) return false; @@ -215,8 +214,7 @@ namespace StardewModdingAPI.Framework.ModLoading // recursively sort dependencies foreach (IModMetadata requiredMod in modsToLoadFirst) { - int index = unsortedMods.IndexOf(requiredMod); - success = this.ProcessDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods); + success = this.ProcessDependencies(requiredMod, visited, sortedMods, currentChain, unsortedMods); if (!success) break; } -- cgit From 2d9aefebb0991b2e942241bf509eaa98f63b4963 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 May 2017 21:19:27 -0400 Subject: rewrite dependency logic to resolve dependency loops by disabling the affected mods (#285) --- src/StardewModdingAPI.Tests/ModResolverTests.cs | 59 ++++++-- .../ModLoading/InvalidModStateException.cs | 14 ++ .../Framework/ModLoading/ModDependencyStatus.cs | 18 +++ .../Framework/ModLoading/ModResolver.cs | 162 ++++++++++++--------- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 + 5 files changed, 178 insertions(+), 77 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index 285c5127..1142a264 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -252,8 +252,8 @@ namespace StardewModdingAPI.Tests // │ │ // └─ C ─┘ Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", modA); - Mock modC = this.GetMetadataForDependencyTest("Mod C", modA, modB); + Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod A", "Mod B" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }).ToArray(); @@ -271,9 +271,9 @@ namespace StardewModdingAPI.Tests // arrange // A ◀── B ◀── C ◀── D Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", modA); - Mock modC = this.GetMetadataForDependencyTest("Mod C", modB); - Mock modD = this.GetMetadataForDependencyTest("Mod D", modC); + Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B" }); + Mock modD = this.GetMetadataForDependencyTest("Mod D", dependencies: new[] { "Mod C" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); @@ -295,11 +295,11 @@ namespace StardewModdingAPI.Tests // │ │ // E ◀── F Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", modA); - Mock modC = this.GetMetadataForDependencyTest("Mod C", modB); - Mock modD = this.GetMetadataForDependencyTest("Mod D", modC); - Mock modE = this.GetMetadataForDependencyTest("Mod E", modB); - Mock modF = this.GetMetadataForDependencyTest("Mod F", modC, modE); + Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B" }); + Mock modD = this.GetMetadataForDependencyTest("Mod D", dependencies: new[] { "Mod C" }); + Mock modE = this.GetMetadataForDependencyTest("Mod E", dependencies: new[] { "Mod B" }); + Mock modF = this.GetMetadataForDependencyTest("Mod F", dependencies: new[] { "Mod C", "Mod E" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }).ToArray(); @@ -314,6 +314,32 @@ namespace StardewModdingAPI.Tests Assert.AreSame(modF.Object, mods[5], "The load order is incorrect: mod F should be last since it needs mods E and C."); } + [Test(Description = "Assert that mods with circular dependency chains are skipped, but any other mods are loaded in the correct order.")] + public void ProcessDependencies_Skips_CircularDependentMods() + { + // arrange + // A ◀── B ◀── C ──▶ D + // ▲ │ + // │ ▼ + // └──── E + Mock modA = this.GetMetadataForDependencyTest("Mod A"); + Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B", "Mod D" }, allowStatusChange: true); + Mock modD = this.GetMetadataForDependencyTest("Mod D", dependencies: new[] { "Mod E" }, allowStatusChange: true); + Mock modE = this.GetMetadataForDependencyTest("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }).ToArray(); + + // assert + Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); + modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); + modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); + } + /********* ** Private methods @@ -338,18 +364,27 @@ namespace StardewModdingAPI.Tests /// Get a randomised basic manifest. /// The mod's name and unique ID. /// The dependencies this mod requires. - private Mock GetMetadataForDependencyTest(string uniqueID, params Mock[] dependencies) + /// Whether the code being tested is allowed to change the mod status. + private Mock GetMetadataForDependencyTest(string uniqueID, string[] dependencies = null, bool allowStatusChange = false) { Mock mod = new Mock(MockBehavior.Strict); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); + mod.Setup(p => p.DisplayName).Returns(uniqueID); mod.Setup(p => p.Manifest).Returns( this.GetRandomManifest(manifest => { manifest.Name = uniqueID; manifest.UniqueID = uniqueID; - manifest.Dependencies = dependencies.Select(p => (IManifestDependency)new ManifestDependency(p.Object.Manifest.UniqueID)).ToArray(); + manifest.Dependencies = dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID)).ToArray(); }) ); + if (allowStatusChange) + { + mod + .Setup(p => p.SetStatus(It.IsAny(), It.IsAny())) + .Callback((status, message) => Console.WriteLine($"<{uniqueID} changed status: [{status}] {message}")) + .Returns(mod.Object); + } return mod; } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs new file mode 100644 index 00000000..ab11272a --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright. + public class InvalidModStateException : Exception + { + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public InvalidModStateException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs new file mode 100644 index 00000000..0774b487 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// The status of a given mod in the dependency-sorting algorithm. + internal enum ModDependencyStatus + { + /// The mod hasn't been visited yet. + Queued, + + /// The mod is currently being analysed as part of a dependency chain. + Checking, + + /// The mod has already been sorted. + Sorted, + + /// The mod couldn't be sorted due to a metadata issue (e.g. missing dependencies). + Failed + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 8efe57d9..2b081edc 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -130,26 +130,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mods to process. public IEnumerable ProcessDependencies(IEnumerable mods) { - var unsortedMods = mods.ToList(); + mods = mods.ToArray(); var sortedMods = new Stack(); - var visitedMods = new HashSet(); - var currentChain = new List(); - bool success = true; - - foreach (IModMetadata mod in unsortedMods) - { - if (mod.Status == ModMetadataStatus.Failed) - continue; - - success = this.ProcessDependencies(mod, visitedMods, sortedMods, currentChain, unsortedMods); - if (!success) - break; - } - - if (!success) - return new ModMetadata[0]; + var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); + foreach (IModMetadata mod in mods) + this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); - return sortedMods.Reverse().ToArray(); + return sortedMods.Reverse(); } @@ -157,73 +144,118 @@ namespace StardewModdingAPI.Framework.ModLoading ** Private methods *********/ /// 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. + /// The full list of mods being validated. /// The mod whose dependencies to process. - /// The mods which have been visited. + /// The dependency state for each mod. /// The list in which to save mods sorted by dependency order. /// The current change of mod dependencies. - /// The mods remaining to sort. - /// Returns whether the mod can be loaded. - private bool ProcessDependencies(IModMetadata mod, HashSet visited, Stack sortedMods, List currentChain, List unsortedMods) + /// Returns the mod dependency status. + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) { - // visit mod - if (visited.Contains(mod)) - return true; // already sorted - visited.Add(mod); + // check if already visited + switch (states[mod]) + { + // already sorted or failed + case ModDependencyStatus.Sorted: + case ModDependencyStatus.Failed: + return states[mod]; - // mod already failed - if (mod.Status == ModMetadataStatus.Failed) - return false; + // dependency loop + case ModDependencyStatus.Checking: + // This should never happen. The higher-level mod checks if the dependency is + // already being checked, so it can fail without visiting a mod twice. If this + // case is hit, that logic didn't catch the dependency loop for some reason. + throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName}))."); - // process dependencies - bool success = true; - if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any()) + // not visited yet, start processing + case ModDependencyStatus.Queued: + break; + + // sanity check + default: + throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); + } + + // no dependencies, mark sorted + if (mod.Manifest.Dependencies == null || !mod.Manifest.Dependencies.Any()) { - // validate required dependencies are present + sortedMods.Push(mod); + return states[mod] = ModDependencyStatus.Sorted; + } + + // missing required dependencies, mark failed + { + string[] missingModIDs = + ( + from dependency in mod.Manifest.Dependencies + where mods.All(m => m.Manifest.UniqueID != dependency.UniqueID) + orderby dependency.UniqueID + select dependency.UniqueID + ) + .ToArray(); + if (missingModIDs.Any()) { - string missingMods = null; - foreach (IManifestDependency dependency in mod.Manifest.Dependencies) - { - if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID))) - missingMods += $"{dependency.UniqueID}, "; - } - if (missingMods != null) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)})."); - return false; - } + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", missingModIDs)})."); + return states[mod] = ModDependencyStatus.Failed; } + } - // get mods which should be loaded before this one + // process dependencies + { + states[mod] = ModDependencyStatus.Checking; + + // get mods to load first IModMetadata[] modsToLoadFirst = ( - from unsorted in unsortedMods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID) - select unsorted + from other in mods + where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest.UniqueID) + select other ) .ToArray(); - // detect circular references - IModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains); - if (circularReferenceMod != null) - { - mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName})."); - return false; - } - currentChain.Add(mod); - // recursively sort dependencies foreach (IModMetadata requiredMod in modsToLoadFirst) { - success = this.ProcessDependencies(requiredMod, visited, sortedMods, currentChain, unsortedMods); - if (!success) - break; + var subchain = new List(currentChain) { mod }; + + // detect dependency loop + if (states[requiredMod] == ModDependencyStatus.Checking) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); + return states[mod] = ModDependencyStatus.Failed; + } + + // recursively process each dependency + var substatus = this.ProcessDependencies(mods, requiredMod, states, sortedMods, subchain); + switch (substatus) + { + // sorted successfully + case ModDependencyStatus.Sorted: + break; + + // failed, which means this mod can't be loaded either + case ModDependencyStatus.Failed: + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); + return states[mod] = ModDependencyStatus.Failed; + + // unexpected status + case ModDependencyStatus.Queued: + case ModDependencyStatus.Checking: + throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status."); + + // sanity check + default: + throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); + } } - } - // mark mod sorted - sortedMods.Push(mod); - currentChain.Remove(mod); - return success; + // all requirements sorted successfully + sortedMods.Push(mod); + return states[mod] = ModDependencyStatus.Sorted; + } } /// Get all mod folders in a root folder, passing through empty folders as needed. diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index a7362153..61b97baa 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -122,6 +122,8 @@ + + -- cgit From fec67fe6fd50f8ac310c55cd4f5bded02d731127 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 May 2017 22:04:32 -0400 Subject: fix world/player events being raised when the player loads a save due to values being initialised --- release-notes.md | 1 + src/StardewModdingAPI/Framework/SGame.cs | 165 +++++++++++++------------------ 2 files changed, 72 insertions(+), 94 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 0a10f57b..261637aa 100644 --- a/release-notes.md +++ b/release-notes.md @@ -57,6 +57,7 @@ For mod developers: * Added a content API which loads custom textures/maps/data from the mod's folder (`.xnb` or `.png` format) or game content. * `Console.Out` messages are now written to the log file. * `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly. +* Fixed value-changed events being raised when the player loads a save due to values being initialised. ## 1.10 See [log](https://github.com/Pathoschild/SMAPI/compare/1.9...1.10). diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 7ee72af2..f318517d 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -90,6 +90,9 @@ namespace StardewModdingAPI.Framework /// The keys that just entered the up state. private Keys[] FrameReleasedKeys => this.PreviouslyPressedKeys.Except(this.CurrentlyPressedKeys).ToArray(); + /// The previous save ID at last check. + private ulong PreviousSaveID; + /// A hash of at last check. private int PreviousGameLocations; @@ -420,107 +423,81 @@ namespace StardewModdingAPI.Framework *********/ if (this.IsWorldReady) { - // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - { - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - this.PreviousGameLocations = this.GetHash(Game1.locations); - } - - // raise current location changed - if (Game1.currentLocation != this.PreviousGameLocation) - { - 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.PreviousGameLocation = Game1.currentLocation; - } - - // raise player changed - if (Game1.player != this.PreviousFarmer) - { - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); - this.PreviousFarmer = Game1.player; - } - - // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); - this.PreviousCombatLevel = Game1.player.combatLevel; - } - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); - this.PreviousFarmingLevel = Game1.player.farmingLevel; - } - if (Game1.player.fishingLevel != this.PreviousFishingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); - this.PreviousFishingLevel = Game1.player.fishingLevel; - } - if (Game1.player.foragingLevel != this.PreviousForagingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); - this.PreviousForagingLevel = Game1.player.foragingLevel; - } - if (Game1.player.miningLevel != this.PreviousMiningLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); - this.PreviousMiningLevel = Game1.player.miningLevel; - } - if (Game1.player.luckLevel != this.PreviousLuckLevel) + // raise events (only when something changes, not on the initial load) + if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); - this.PreviousLuckLevel = 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.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); - } + // raise location list changed + if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - // raise current location's object list changed - { - int? objectHash = Game1.currentLocation?.objects != null ? this.GetHash(Game1.currentLocation.objects) : (int?)null; - if (objectHash != null && this.PreviousLocationObjects != objectHash) + // raise current location changed + if (Game1.currentLocation != this.PreviousGameLocation) { - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); - this.PreviousLocationObjects = objectHash.Value; + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); + LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); } - } - // raise time changed - if (Game1.timeOfDay != this.PreviousTime) - { - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); - this.PreviousTime = Game1.timeOfDay; - } - if (Game1.dayOfMonth != this.PreviousDay) - { - TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); - this.PreviousDay = Game1.dayOfMonth; - } - if (Game1.currentSeason != this.PreviousSeason) - { - TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); - this.PreviousSeason = Game1.currentSeason; - } - if (Game1.year != this.PreviousYear) - { - TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); - this.PreviousYear = Game1.year; - } + // raise player changed + if (Game1.player != this.PreviousFarmer) + PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); + + // raise player leveled up a skill + if (Game1.player.combatLevel != this.PreviousCombatLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + if (Game1.player.farmingLevel != this.PreviousFarmingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + if (Game1.player.fishingLevel != this.PreviousFishingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + if (Game1.player.foragingLevel != this.PreviousForagingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + if (Game1.player.miningLevel != this.PreviousMiningLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + if (Game1.player.luckLevel != this.PreviousLuckLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, 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); + + // raise current location's object list changed + if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) + LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); - // raise mine level changed - if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - { - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); - this.PreviousMineLevel = Game1.mine.mineLevel; + // raise time changed + if (Game1.timeOfDay != this.PreviousTime) + TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + if (Game1.dayOfMonth != this.PreviousDay) + TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); + if (Game1.currentSeason != this.PreviousSeason) + TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); + if (Game1.year != this.PreviousYear) + TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); + + // raise mine level changed + if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) + MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); } + + // update state + this.PreviousGameLocations = this.GetHash(Game1.locations); + this.PreviousGameLocation = Game1.currentLocation; + this.PreviousFarmer = Game1.player; + this.PreviousCombatLevel = Game1.player.combatLevel; + this.PreviousFarmingLevel = Game1.player.farmingLevel; + this.PreviousFishingLevel = Game1.player.fishingLevel; + this.PreviousForagingLevel = Game1.player.foragingLevel; + this.PreviousMiningLevel = Game1.player.miningLevel; + this.PreviousLuckLevel = Game1.player.luckLevel; + this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); + this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); + this.PreviousTime = Game1.timeOfDay; + this.PreviousDay = Game1.dayOfMonth; + this.PreviousSeason = Game1.currentSeason; + this.PreviousYear = Game1.year; + this.PreviousMineLevel = Game1.mine.mineLevel; + this.PreviousSaveID = Game1.uniqueIDForThisGame; } /********* -- cgit From aafb3315cb20e17682569d19ae85d333b13cd3e0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 May 2017 22:41:44 -0400 Subject: update compatibility blacklist for SDV 1.2 --- release-notes.md | 1 + src/StardewModdingAPI/Framework/ModRegistry.cs | 2 +- .../Framework/Models/ModCompatibility.cs | 7 +- src/StardewModdingAPI/Program.cs | 2 +- .../StardewModdingAPI.config.json | 307 +++++++++++++++------ 5 files changed, 228 insertions(+), 91 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 261637aa..d9d6a12a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -19,6 +19,7 @@ For players: * Fixed installer finding redundant game paths on Linux. * Fixed save events not being raised after the first day on Linux/Mac. * Fixed error on Linux/Mac when a mod loads a PNG immediately after the save is loaded. +* Updated mod compatibility list for Stardew Valley 1.2. For mod developers: * Added log entries for basic context changes (e.g. loaded save) to simplify troubleshooting. diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index f015b7ba..c2a8b2ef 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Framework return ( from mod in this.CompatibilityRecords where - mod.ID == key + mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) select mod diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index 1e71dae0..90cbd237 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -12,8 +12,8 @@ namespace StardewModdingAPI.Framework.Models /**** ** From config ****/ - /// The unique mod ID. - public string ID { get; set; } + /// The unique mod IDs. + public string[] ID { get; set; } /// The mod name. public string Name { get; set; } @@ -24,6 +24,9 @@ namespace StardewModdingAPI.Framework.Models /// The most recent incompatible mod version. public string UpperVersion { get; set; } + /// A label to show to the user instead of , when the manifest version differs from the user-facing version. + public string UpperVersionLabel { get; set; } + /// The URL the user can check for an official updated version. public string UpdateUrl { get; set; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 75bdba0f..1678c5fa 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -533,7 +533,7 @@ namespace StardewModdingAPI bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:"; if (hasOfficialUrl) error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; if (hasUnofficialUrl) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index f42a4dfc..08bd3cff 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -34,251 +34,384 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ModCompatibility": [ { "Name": "AccessChestAnywhere", - "ID": "AccessChestAnywhere", + "ID": [ "AccessChestAnywhere" ], "UpperVersion": "1.1", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + "Notes": "Needs update for SDV 1.1." }, { "Name": "Almighty Tool", - "ID": "AlmightyTool.dll", + "ID": [ "AlmightyTool.dll" ], "UpperVersion": "1.1.1", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." + "Notes": "Needs update for SDV 1.2." }, { "Name": "Better Sprinklers", - "ID": "SPDSprinklersMod", + "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], "UpperVersion": "2.3", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." }, { - "Name": "Better Sprinklers", - "ID": "Speeder.BetterSprinklers", - "UpperVersion": "2.3", + "Name": "Birthday Mail", + "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], + "UpperVersion": "1.2.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "Notes": "ID changed in 2.3. Uses obsolete StardewModdingAPI.Extensions." + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/276", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Chest Label System", - "ID": "SPDChestLabel", - "UpperVersion": "1.5", + "ID": [ "SPDChestLabel" ], + "UpperVersion": "1.6", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", - "Notes": "Not compatible with Stardew Valley 1.1+" + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.1." }, { - "Name": "Chests Anywhere", - "ID": "ChestsAnywhere", - "UpperVersion": "1.8.2", + "Name": "Chest Pooling", + "ID": [ "ChestPooling.dll" ], + "UpperVersion": "1.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'." + "UpdateUrl": "http://community.playstarbound.com/threads/111988", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Chests Anywhere", - "ID": "Pathoschild.ChestsAnywhere", + "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ], "UpperVersion": "1.9-beta", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'." + "Notes": "Needs update for SDV 1.2." }, { "Name": "CJB Automation", - "ID": "CJBAutomation", + "ID": [ "CJBAutomation" ], "UpperVersion": "1.4", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", + "Notes": "Needs update for SDV 1.2." }, { "Name": "CJB Cheats Menu", - "ID": "CJBCheatsMenu", + "ID": [ "CJBCheatsMenu" ], "UpperVersion": "1.12", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", - "Notes": "Not compatible with Stardew Valley 1.1+" + "Notes": "Needs update for SDV 1.1." }, { "Name": "CJB Item Spawner", - "ID": "CJBItemSpawner", + "ID": [ "CJBItemSpawner" ], "UpperVersion": "1.5", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", - "Notes": "Not compatible with Stardew Valley 1.1+" + "Notes": "Needs update for SDV 1.1." }, { "Name": "CJB Show Item Sell Price", - "ID": "CJBShowItemSellPrice", + "ID": [ "CJBShowItemSellPrice" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Needs update for SDV 1.2." }, { "Name": "Cooking Skill", - "ID": "CookingSkill", + "ID": [ "CookingSkill" ], "UpperVersion": "1.0.3", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", - "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'." + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Cooking Skill Prestige Adapter", + "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], + "UpperVersion": "1.0.4", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/569", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Enemy Health Bars", - "ID": "SPDHealthBar", + "ID": [ "SPDHealthBar" ], "UpperVersion": "1.7", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193", - "Notes": "Uses obsolete GraphicsEvents.DrawTick." + "UnofficialUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Entoarox Framework", - "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9", - "UpperVersion": "1.6.5", + "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], + "UpperVersion": "1.7.5", "Compatibility": "AssumeBroken", "UpdateUrl": "http://community.playstarbound.com/resources/4228", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')." + "Notes": "Needs update for SDV 1.2." }, { "Name": "Extended Fridge", - "ID": "Mystra007ExtendedFridge", + "ID": [ "Mystra007ExtendedFridge" ], "UpperVersion": "1.0", + "UpperVersionLabel": "0.94", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", - "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'." + "Notes": "Needs update for SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." + }, + { + "Name": "FarmAutomation.ItemCollector", + "ID": [ "FarmAutomation.ItemCollector.dll", /*since 0.4*/ "Maddy99.FarmAutomation.ItemCollector" ], + "UpperVersion": "0.4", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/threads/125172", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Instant Geode", + "ID": [ "InstantGeode" ], + "UpperVersion": "1.12", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/threads/109038", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Gate Opener", + "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ], + "UpperVersion": "1.0.1", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/threads/111988", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Get Dressed", - "ID": "GetDressed.dll", - "UpperVersion": "3.2", + "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], + "UpperVersion": "3.3", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", - "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick." + "Notes": "Needs update for SDV 1.2." }, { - "Name": "Lookup Anything", - "ID": "LookupAnything", - "UpperVersion": "1.10", + "Name": "Gift Taste Helper", + "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ], + "UpperVersion": "2.3.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "Crashes with FormatException when looking up NPCs." + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/229", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Lookup Anything", - "ID": "Pathoschild.LookupAnything", + "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], "UpperVersion": "1.10.1", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs." + "Notes": "Needs update for SDV 1.2." }, { "Name": "Makeshift Multiplayer", - "ID": "StardewValleyMP", + "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], "Compatibility": "AssumeBroken", - "UpperVersion": "0.2.10", + "UpperVersion": "0.3.3", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501", - "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck." + "Notes": "Needs update for SDV 1.2." }, { "Name": "NoSoilDecay", - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], "UpperVersion": "0.5", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", - "Notes": "Uses obsolete StardewModdingAPI.Extensions and Assembly.GetExecutingAssembly().Location." + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." }, { "Name": "NPC Map Locations", - "ID": "NPCMapLocationsMod", + "ID": [ "NPCMapLocationsMod" ], "LowerVersion": "1.42", "UpperVersion": "1.43", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", - "ReasonPhrase": "this version has an update check error which crashes the game" + "ReasonPhrase": "These versions have an update check error which crash the game." + }, + { + "Name": "Part of the Community", + "ID": [ "SB_PotC" ], + "UpperVersion": "1.0.8", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/923", + "ReasonPhrase": "Needs update for SDV 1.2." }, { "Name": "Point-and-Plant", - "ID": "PointAndPlant.dll", + "ID": [ "PointAndPlant.dll" ], "UpperVersion": "1.0.2", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "PrairieKingMadeEasy", + "ID": [ "PrairieKingMadeEasy.dll" ], + "UpperVersion": "1.0.0", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/resources/3594", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Rush Orders", + "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], + "UpperVersion": "1.1", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/605", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Save Anywhere", - "ID": "SaveAnywhere", - "UpperVersion": "2.0", + "ID": [ "SaveAnywhere" ], + "UpperVersion": "2.3", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444", - "Notes": "Depends on StarDustCore." + "Notes": "Needs update for SDV 1.2." }, { - "Name": "StackSplitX", - "ID": "StackSplitX.dll", + "Name": "Simple Sprinklers", + "ID": [ "SimpleSprinkler.dll" ], + "UpperVersion": "1.4", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/76", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Sprint and Dash", + "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.0", "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/resources/3531", + "UnofficialUpdateUrl": "http://community.playstarbound.com/resources/4201", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Sprint and Dash Redux", + "ID": [ "SPDSprintAndDash" ], + "UpperVersion": "1.2", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/resources/4201", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "StackSplitX", + "ID": [ "StackSplitX.dll" ], + "UpperVersion": "1.2", + "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Needs update for SDV 1.2." }, { "Name": "StarDustCore", - "ID": "StarDustCore", + "ID": [ "StarDustCore" ], "UpperVersion": "1.0", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683", - "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'." + "Notes": "Obsolete (originally needed by Save Anywhere); broken in SDV 1.2." }, { "Name": "Teleporter", - "ID": "Teleporter", + "ID": [ "Teleporter" ], "UpperVersion": "1.0.2", "Compatibility": "AssumeBroken", "UpdateUrl": "http://community.playstarbound.com/resources/4374", - "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'." + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "UiModSuite", + "ID": [ "Demiacle.UiModSuite" ], + "UpperVersion": "0.5", + "UpperVersionLabel": "1.0", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1023", + "Notes": "Needs update for SDV 1.2. Actual upper version is 1.0, but mod incorrectly sets it to 0.5 in the manifest." + }, + { + "Name": "Weather Controller", + "ID": [ "WeatherController.dll" ], + "UpperVersion": "1.0.2", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/threads/111526", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "zDailyIncrease", + "ID": [ "zdailyincrease" ], + "UpperVersion": "1.2", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/resources/4247", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Zoom Out Extreme", + "ID": [ "ZoomMod" ], + "UpperVersion": "0.1", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://community.playstarbound.com/threads/115028", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Zoryn's Better RNG", - "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", - "UpperVersion": "1.5", + "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ], + "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Zoryn's Calendar Anywhere", - "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a", - "UpperVersion": "1.5", + "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ], + "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Zoryn's Health Bars", - "ID": "HealthBars.dll", - "UpperVersion": "1.5", + "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ], + "UpperVersion": "1.6", + "Compatibility": "AssumeBroken", + "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "Notes": "Needs update for SDV 1.2." + }, + { + "Name": "Zoryn's Junimo Deposit Anywhere", + "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ], + "UpperVersion": "1.7", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Zoryn's Movement Mod", - "ID": "8a632929-8335-484f-87dd-c29d2ba3215d", - "UpperVersion": "1.5", + "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ], + "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "Notes": "Needs update for SDV 1.2." }, { "Name": "Zoryn's Regen Mod", - "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", - "UpperVersion": "1.5", + "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ], + "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "Notes": "Needs update for SDV 1.2." } ] } -- cgit From bca78cd682c0d583913811f632815db142fbde8b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 May 2017 22:51:49 -0400 Subject: add Context.IsWorldReady flag --- release-notes.md | 2 ++ src/StardewModdingAPI/Context.cs | 17 +++++++++++++---- src/StardewModdingAPI/Framework/SGame.cs | 10 +++++----- 3 files changed, 20 insertions(+), 9 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index d9d6a12a..507fb1af 100644 --- a/release-notes.md +++ b/release-notes.md @@ -22,6 +22,8 @@ For players: * Updated mod compatibility list for Stardew Valley 1.2. For mod developers: +* Added a `Context.IsWorldReady` flag. + _This is set to `true` when the player has loaded a save and the world is finished initialising. This is set at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised, and is mainly useful with events which can be raised before the world is loaded._ * Added log entries for basic context changes (e.g. loaded save) to simplify troubleshooting. * Added a `debug` console command to TrainerMod which lets you pass debug commands to the game (e.g. `debug warp FarmHouse 1 1` warps the player to the farmhouse). * Added a deprecation warning for mods that don't set the `UniqueID` manifest field, which will be required in SMAPI 2.0. diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 45f6a05f..6bc5ae56 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -4,18 +4,27 @@ using StardewValley.Menus; namespace StardewModdingAPI { /// Provides information about the current game state. - internal static class Context + public static class Context { /********* ** Accessors *********/ + /**** + ** Public + ****/ + /// Whether the player has loaded a save and the world has finished initialising. + public static bool IsWorldReady { get; internal set; } + + /**** + ** Internal + ****/ /// Whether a player save has been loaded. - public static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); /// Whether the game is currently writing to the save file. - public static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something + internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something /// Whether the game is currently running the draw loop. - public static bool IsInDrawLoop { get; set; } + internal static bool IsInDrawLoop { get; set; } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f318517d..4f3a29fc 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -39,9 +39,6 @@ namespace StardewModdingAPI.Framework /// The number of consecutive failed draws. private int FailedDraws; - /// Whether the player has loaded a save and the world has finished initialising. - private bool IsWorldReady => this.AfterLoadTimer < 0; - /// Whether the game is returning to the menu. private bool IsExiting; @@ -309,6 +306,7 @@ namespace StardewModdingAPI.Framework if (this.AfterLoadTimer == 0) { 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); PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); @@ -325,9 +323,11 @@ namespace StardewModdingAPI.Framework this.IsExiting = true; // after exit to title - if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) + if (Context.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); + Context.IsWorldReady = false; + SaveEvents.InvokeAfterReturnToTitle(this.Monitor); this.AfterLoadTimer = 5; this.IsExiting = false; @@ -421,7 +421,7 @@ namespace StardewModdingAPI.Framework /********* ** World & player events *********/ - if (this.IsWorldReady) + if (Context.IsWorldReady) { // raise events (only when something changes, not on the initial load) if (Game1.uniqueIDForThisGame == this.PreviousSaveID) -- cgit From 11569dac318a60bedd460b29a3a7991d21db32d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 May 2017 19:15:28 -0400 Subject: fix maps not recognising custom tilesheets added through the SMAPI content API --- release-notes.md | 5 +++-- src/StardewModdingAPI/Framework/SGame.cs | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index d10ce4e1..abcce4d8 100644 --- a/release-notes.md +++ b/release-notes.md @@ -22,13 +22,14 @@ For players: * Updated mod compatibility list for Stardew Valley 1.2. For mod developers: +* SMAPI now logs basic context info to simplify troubleshooting. * Added a `Context.IsWorldReady` flag. - _This is set to `true` when the player has loaded a save and the world is finished initialising. This is set at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised, and is mainly useful with events which can be raised before the world is loaded._ -* Added log entries for basic context changes (e.g. loaded save) to simplify troubleshooting. + _This flag is true when a save is loaded and the world is finished initialising, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful with events which can be raised before the world is loaded (like update tick)._ * Added a `debug` console command to TrainerMod which lets you pass debug commands to the game (e.g. `debug warp FarmHouse 1 1` warps the player to the farmhouse). * Added a deprecation warning for mods that don't set the `Name`, `Version`, or `UniqueID` manifest fields. These will be required in SMAPI 2.0. * Mods can now override `Dispose` if they need to release unmanaged resources. * Deprecated `GameEvents.GameLoaded` and `GameEvents.FirstUpdateTick`, since any logic in the mod's `Entry` method will happen after the game is loaded. +* Fixed maps not recognising custom tilesheets added through the SMAPI content API. ## 1.12 See [log](https://github.com/Pathoschild/SMAPI/compare/1.11...1.12). diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 4f3a29fc..a340b995 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -168,7 +168,7 @@ namespace StardewModdingAPI.Framework private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static float _fps { - set { SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); } + set => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); } private static Task _newDayTask => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_newDayTask)).GetValue(); private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); @@ -202,6 +202,15 @@ namespace StardewModdingAPI.Framework SGame.Reflection = reflection; Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley + + // The game uses the default content manager instead of Game1.CreateContentManager in + // several cases (See http://community.playstarbound.com/threads/130058/page-27#post-3159274). + // The workaround is... + // 1. Override the default content manager. + // 2. Since Game1.content isn't initialised yet, and we need one main instance to + // support custom map tilesheets, detect when Game1.content is being initialised + // and use the same instance. + this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); } /**** @@ -212,6 +221,12 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { + // When Game1.content is being initialised, use SMAPI's main content manager instance. + // See comment in SGame constructor. + if (Game1.content == null && this.Content is SContentManager mainContentManager) + return mainContentManager; + + // build new instance return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); } -- cgit From 89d7a3f846b3f380c43656e02ba645d57bef03ea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 May 2017 23:04:38 -0400 Subject: add compile flag for experimental mod dependencies features --- src/StardewModdingAPI.Tests/ModResolverTests.cs | 8 ++++++++ src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 4 ++++ src/StardewModdingAPI/Framework/Models/Manifest.cs | 2 ++ src/StardewModdingAPI/IManifest.cs | 2 ++ src/StardewModdingAPI/Program.cs | 2 ++ 5 files changed, 18 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index 1142a264..efa6fa06 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -70,7 +70,9 @@ namespace StardewModdingAPI.Tests [nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}", [nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll", [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}", +#if EXPERIMENTAL [nameof(IManifest.Dependencies)] = new[] { originalDependency }, +#endif ["ExtraString"] = Sample.String(), ["ExtraInt"] = Sample.Int() }; @@ -107,9 +109,11 @@ namespace StardewModdingAPI.Tests Assert.AreEqual(original["ExtraString"], mod.Manifest.ExtraFields["ExtraString"], "The manifest's extra fields should contain an 'ExtraString' value."); Assert.AreEqual(original["ExtraInt"], mod.Manifest.ExtraFields["ExtraInt"], "The manifest's extra fields should contain an 'ExtraInt' value."); +#if EXPERIMENTAL Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null."); Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value."); Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match."); +#endif } /**** @@ -211,6 +215,7 @@ namespace StardewModdingAPI.Tests // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. } +#if EXPERIMENTAL /**** ** ProcessDependencies ****/ @@ -339,6 +344,7 @@ namespace StardewModdingAPI.Tests modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); } +#endif /********* @@ -361,6 +367,7 @@ namespace StardewModdingAPI.Tests return manifest; } +#if EXPERIMENTAL /// Get a randomised basic manifest. /// The mod's name and unique ID. /// The dependencies this mod requires. @@ -387,5 +394,6 @@ namespace StardewModdingAPI.Tests } return mod; } +#endif } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 00d4448b..53c28ace 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -126,6 +126,7 @@ namespace StardewModdingAPI.Framework.ModLoading } } +#if EXPERIMENTAL /// Sort the given mods by the order they should be loaded. /// The mods to process. public IEnumerable ProcessDependencies(IEnumerable mods) @@ -138,11 +139,13 @@ namespace StardewModdingAPI.Framework.ModLoading return sortedMods.Reverse(); } +#endif /********* ** Private methods *********/ +#if EXPERIMENTAL /// 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. /// The full list of mods being validated. /// The mod whose dependencies to process. @@ -257,6 +260,7 @@ namespace StardewModdingAPI.Framework.ModLoading return states[mod] = ModDependencyStatus.Sorted; } } +#endif /// Get all mod folders in a root folder, passing through empty folders as needed. /// The root folder path to search. diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index be781585..53384852 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -30,9 +30,11 @@ namespace StardewModdingAPI.Framework.Models /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } +#if EXPERIMENTAL /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestFieldConverter))] public IManifestDependency[] Dependencies { get; set; } +#endif /// The unique mod ID. public string UniqueID { get; set; } diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 9533aadb..c036fdd3 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -29,8 +29,10 @@ namespace StardewModdingAPI /// The name of the DLL in the directory that has the method. string EntryDll { get; } +#if EXPERIMENTAL /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } +#endif /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 228071ce..7a8189bd 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -357,8 +357,10 @@ namespace StardewModdingAPI } } +#if EXPERIMENTAL // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); +#endif // load mods modsLoaded = this.LoadMods(mods, new JsonHelper(), (SContentManager)Game1.content, deprecationWarnings); -- cgit From 8439594b10aad22c098c4d3af44daf710b2e59ac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 11:45:17 -0400 Subject: fix errors during overridden update loop immediately crashing the game with no log entry --- release-notes.md | 1 + src/StardewModdingAPI/Framework/Countdown.cs | 44 ++ src/StardewModdingAPI/Framework/SGame.cs | 599 +++++++++++++------------ src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 352 insertions(+), 293 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Countdown.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 621854ba..0177a388 100644 --- a/release-notes.md +++ b/release-notes.md @@ -15,6 +15,7 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.12...1.13). For players: * SMAPI now recovers better from mod draw errors and detects when the error is irrecoverable. +* SMAPI now recovers automatically from update errors if possible. * SMAPI now remembers if your game crashed and offers help next time you launch it. * Fixed installer finding redundant game paths on Linux. * Fixed save events not being raised after the first day on Linux/Mac. diff --git a/src/StardewModdingAPI/Framework/Countdown.cs b/src/StardewModdingAPI/Framework/Countdown.cs new file mode 100644 index 00000000..25ca2546 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Countdown.cs @@ -0,0 +1,44 @@ +namespace StardewModdingAPI.Framework +{ + /// Counts down from a baseline value. + internal class Countdown + { + /********* + ** Accessors + *********/ + /// The initial value from which to count down. + public int Initial { get; } + + /// The current value. + public int Current { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial value from which to count down. + public Countdown(int initial) + { + this.Initial = initial; + this.Current = initial; + } + + /// Reduce the current value by one. + /// Returns whether the value was decremented (i.e. wasn't already zero). + public bool Decrement() + { + if (this.Current <= 0) + return false; + + this.Current--; + return true; + } + + /// Restart the countdown. + public void Reset() + { + this.Current = this.Initial; + } + } +} diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index a340b995..cb7c0e7b 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -30,15 +30,15 @@ namespace StardewModdingAPI.Framework ** SMAPI state ****/ /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. - private readonly int MaxFailedDraws = 60; // roughly one second + private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second + + /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. + private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second /// The number of ticks until SMAPI should notify mods that the game has loaded. /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; - /// The number of consecutive failed draws. - private int FailedDraws; - /// Whether the game is returning to the menu. private bool IsExiting; @@ -234,339 +234,353 @@ namespace StardewModdingAPI.Framework /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) { - /********* - ** Skip conditions - *********/ - // SMAPI exiting, stop processing game updates - if (this.Monitor.IsExiting) + try { - this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); - return; - } + /********* + ** Skip conditions + *********/ + // SMAPI exiting, stop processing game updates + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); + return; + } - // While a background new-day task is in progress, the game skips its own update logic - // and defers to the XNA Update method. Running mod code in parallel to the background - // update is risky, because data changes can conflict (e.g. collection changed during - // enumeration errors) and data may change unexpectedly from one mod instruction to the - // next. - // - // Therefore we can just run Game1.Update here without raising any SMAPI events. There's - // a small chance that the task will finish after we defer but before the game checks, - // which means technically events should be raised, but the effects of missing one - // update tick are neglible and not worth the complications of bypassing Game1.Update. - if (SGame._newDayTask != null) - { - base.Update(gameTime); - return; - } + // While a background new-day task is in progress, the game skips its own update logic + // and defers to the XNA Update method. Running mod code in parallel to the background + // update is risky, because data changes can conflict (e.g. collection changed during + // enumeration errors) and data may change unexpectedly from one mod instruction to the + // next. + // + // Therefore we can just run Game1.Update here without raising any SMAPI events. There's + // a small chance that the task will finish after we defer but before the game checks, + // which means technically events should be raised, but the effects of missing one + // update tick are neglible and not worth the complications of bypassing Game1.Update. + if (SGame._newDayTask != null) + { + base.Update(gameTime); + return; + } - // While the game is writing to the save file in the background, mods can unexpectedly - // fail since they don't have exclusive access to resources (e.g. collection changed - // during enumeration errors). To avoid problems, events are not invoked while a save - // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is - // opened (since the save hasn't started yet), but all other events should be suppressed. - if (Context.IsSaving) - { - // raise before-save - if (!this.IsBetweenSaveEvents) + // While the game is writing to the save file in the background, mods can unexpectedly + // fail since they don't have exclusive access to resources (e.g. collection changed + // during enumeration errors). To avoid problems, events are not invoked while a save + // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is + // opened (since the save hasn't started yet), but all other events should be suppressed. + if (Context.IsSaving) + { + // raise before-save + if (!this.IsBetweenSaveEvents) + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save.", LogLevel.Trace); + SaveEvents.InvokeBeforeSave(this.Monitor); + } + + // suppress non-save events + base.Update(gameTime); + return; + } + if (this.IsBetweenSaveEvents) { - this.IsBetweenSaveEvents = true; - this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); + // 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); } - // suppress non-save events - base.Update(gameTime); - return; - } - 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); - } + /********* + ** Game loaded events + *********/ + if (this.FirstUpdate) + { + GameEvents.InvokeInitialize(this.Monitor); + GameEvents.InvokeLoadContent(this.Monitor); + GameEvents.InvokeGameLoaded(this.Monitor); + } - /********* - ** Game loaded events - *********/ - if (this.FirstUpdate) - { - GameEvents.InvokeInitialize(this.Monitor); - GameEvents.InvokeLoadContent(this.Monitor); - GameEvents.InvokeGameLoaded(this.Monitor); - } + /********* + ** Locale changed events + *********/ + if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) + { + var oldValue = this.PreviousLocale; + var newValue = LocalizedContentManager.CurrentLanguageCode; - /********* - ** Locale changed events - *********/ - if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) - { - var oldValue = this.PreviousLocale; - var newValue = LocalizedContentManager.CurrentLanguageCode; + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + if (oldValue != null) + ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.PreviousLocale = newValue; + } - if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); - this.PreviousLocale = newValue; - } + /********* + ** After load events + *********/ + if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) + { + if (this.AfterLoadTimer == 0) + { + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + Context.IsWorldReady = true; - /********* - ** After load events - *********/ - if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) - { - if (this.AfterLoadTimer == 0) + SaveEvents.InvokeAfterLoad(this.Monitor); + PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + this.AfterLoadTimer--; + } + + /********* + ** Exit to title events + *********/ + // before exit to title + if (Game1.exitToTitle) + this.IsExiting = true; + + // after exit to title + if (Context.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - Context.IsWorldReady = true; + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + Context.IsWorldReady = false; - SaveEvents.InvokeAfterLoad(this.Monitor); - PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); - TimeEvents.InvokeAfterDayStarted(this.Monitor); + SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + this.AfterLoadTimer = 5; + this.IsExiting = false; } - this.AfterLoadTimer--; - } - /********* - ** Exit to title events - *********/ - // before exit to title - if (Game1.exitToTitle) - this.IsExiting = true; + /********* + ** Input events + *********/ + { + // get latest state + this.KStateNow = Keyboard.GetState(); + this.MStateNow = Mouse.GetState(); + this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); - // after exit to title - if (Context.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) - { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - Context.IsWorldReady = false; + // raise key pressed + foreach (var key in this.FramePressedKeys) + ControlEvents.InvokeKeyPressed(this.Monitor, key); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); - this.AfterLoadTimer = 5; - this.IsExiting = false; - } + // raise key released + foreach (var key in this.FrameReleasedKeys) + ControlEvents.InvokeKeyReleased(this.Monitor, key); - /********* - ** Input events - *********/ - { - // get latest state - this.KStateNow = Keyboard.GetState(); - this.MStateNow = Mouse.GetState(); - this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); + // raise controller button pressed + for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + { + var buttons = this.GetFramePressedButtons(i); + foreach (var button in buttons) + { + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) + ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + else + ControlEvents.InvokeButtonPressed(this.Monitor, i, button); + } + } - // raise key pressed - foreach (var key in this.FramePressedKeys) - ControlEvents.InvokeKeyPressed(this.Monitor, key); + // raise controller button released + for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + { + foreach (var button in this.GetFrameReleasedButtons(i)) + { + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) + ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + else + ControlEvents.InvokeButtonReleased(this.Monitor, i, button); + } + } - // raise key released - foreach (var key in this.FrameReleasedKeys) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + // raise keyboard state changed + if (this.KStateNow != this.KStatePrior) + ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); - // raise controller button pressed - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - { - var buttons = this.GetFramePressedButtons(i); - foreach (var button in buttons) + // raise mouse state changed + if (this.MStateNow != this.MStatePrior) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonPressed(this.Monitor, i, button); + ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); + this.MStatePrior = this.MStateNow; + this.MPositionPrior = this.MPositionNow; } } - // raise controller button released - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + /********* + ** Menu events + *********/ + if (Game1.activeClickableMenu != this.PreviousActiveMenu) { - foreach (var button in this.GetFrameReleasedButtons(i)) + IClickableMenu previousMenu = this.PreviousActiveMenu; + IClickableMenu newMenu = Game1.activeClickableMenu; + + // log context + if (this.VerboseLogging) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + if (previousMenu == null) + this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); + else if (newMenu == null) + this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); else - ControlEvents.InvokeButtonReleased(this.Monitor, i, button); + this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); } - } - // raise keyboard state changed - if (this.KStateNow != this.KStatePrior) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); + // raise menu events + if (newMenu != null) + MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + else + MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); - // raise mouse state changed - if (this.MStateNow != this.MStatePrior) - { - ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); - this.MStatePrior = this.MStateNow; - this.MPositionPrior = this.MPositionNow; + // 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) + this.PreviousActiveMenu = newMenu; } - } - /********* - ** Menu events - *********/ - if (Game1.activeClickableMenu != this.PreviousActiveMenu) - { - IClickableMenu previousMenu = this.PreviousActiveMenu; - IClickableMenu newMenu = Game1.activeClickableMenu; - - // log context - if (this.VerboseLogging) + /********* + ** World & player events + *********/ + if (Context.IsWorldReady) { - if (previousMenu == null) - this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); - else if (newMenu == null) - this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); - else - this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); - } - - // raise menu events - if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); - else - MenuEvents.InvokeMenuClosed(this.Monitor, 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) - this.PreviousActiveMenu = newMenu; - } + // raise events (only when something changes, not on the initial load) + if (Game1.uniqueIDForThisGame == this.PreviousSaveID) + { + // raise location list changed + if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - /********* - ** World & player events - *********/ - if (Context.IsWorldReady) - { - // raise events (only when something changes, not on the initial load) - if (Game1.uniqueIDForThisGame == this.PreviousSaveID) - { - // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + // raise current location changed + if (Game1.currentLocation != this.PreviousGameLocation) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); + LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + } - // raise current location changed - if (Game1.currentLocation != this.PreviousGameLocation) - { - if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + // raise player changed + if (Game1.player != this.PreviousFarmer) + PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); + + // raise player leveled up a skill + if (Game1.player.combatLevel != this.PreviousCombatLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + if (Game1.player.farmingLevel != this.PreviousFarmingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + if (Game1.player.fishingLevel != this.PreviousFishingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + if (Game1.player.foragingLevel != this.PreviousForagingLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + if (Game1.player.miningLevel != this.PreviousMiningLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + if (Game1.player.luckLevel != this.PreviousLuckLevel) + PlayerEvents.InvokeLeveledUp(this.Monitor, 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); + + // raise current location's object list changed + if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) + LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + + // raise time changed + if (Game1.timeOfDay != this.PreviousTime) + TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + if (Game1.dayOfMonth != this.PreviousDay) + TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); + if (Game1.currentSeason != this.PreviousSeason) + TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); + if (Game1.year != this.PreviousYear) + TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); + + // raise mine level changed + if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) + MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); } - // raise player changed - if (Game1.player != this.PreviousFarmer) - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); - - // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); - if (Game1.player.fishingLevel != this.PreviousFishingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); - if (Game1.player.foragingLevel != this.PreviousForagingLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); - if (Game1.player.miningLevel != this.PreviousMiningLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); - if (Game1.player.luckLevel != this.PreviousLuckLevel) - PlayerEvents.InvokeLeveledUp(this.Monitor, 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); - - // raise current location's object list changed - if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); - - // raise time changed - if (Game1.timeOfDay != this.PreviousTime) - TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); - if (Game1.dayOfMonth != this.PreviousDay) - TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); - if (Game1.currentSeason != this.PreviousSeason) - TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); - if (Game1.year != this.PreviousYear) - TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); - - // raise mine level changed - if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) - MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + // update state + this.PreviousGameLocations = this.GetHash(Game1.locations); + this.PreviousGameLocation = Game1.currentLocation; + this.PreviousFarmer = Game1.player; + this.PreviousCombatLevel = Game1.player.combatLevel; + this.PreviousFarmingLevel = Game1.player.farmingLevel; + this.PreviousFishingLevel = Game1.player.fishingLevel; + this.PreviousForagingLevel = Game1.player.foragingLevel; + this.PreviousMiningLevel = Game1.player.miningLevel; + this.PreviousLuckLevel = Game1.player.luckLevel; + this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); + this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); + this.PreviousTime = Game1.timeOfDay; + this.PreviousDay = Game1.dayOfMonth; + this.PreviousSeason = Game1.currentSeason; + this.PreviousYear = Game1.year; + this.PreviousMineLevel = Game1.mine.mineLevel; + this.PreviousSaveID = Game1.uniqueIDForThisGame; } - // update state - this.PreviousGameLocations = this.GetHash(Game1.locations); - this.PreviousGameLocation = Game1.currentLocation; - this.PreviousFarmer = Game1.player; - this.PreviousCombatLevel = Game1.player.combatLevel; - this.PreviousFarmingLevel = Game1.player.farmingLevel; - this.PreviousFishingLevel = Game1.player.fishingLevel; - this.PreviousForagingLevel = Game1.player.foragingLevel; - this.PreviousMiningLevel = Game1.player.miningLevel; - this.PreviousLuckLevel = Game1.player.luckLevel; - this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); - this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); - this.PreviousTime = Game1.timeOfDay; - this.PreviousDay = Game1.dayOfMonth; - this.PreviousSeason = Game1.currentSeason; - this.PreviousYear = Game1.year; - this.PreviousMineLevel = Game1.mine.mineLevel; - this.PreviousSaveID = Game1.uniqueIDForThisGame; - } + /********* + ** Game day transition event (obsolete) + *********/ + if (Game1.newDay != this.PreviousIsNewDay) + { + TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); + this.PreviousIsNewDay = Game1.newDay; + } - /********* - ** Game day transition event (obsolete) - *********/ - if (Game1.newDay != this.PreviousIsNewDay) - { - TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); - this.PreviousIsNewDay = Game1.newDay; - } + /********* + ** Game update + *********/ + try + { + base.Update(gameTime); + } + catch (Exception ex) + { + this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + } - /********* - ** Game update - *********/ - try - { - base.Update(gameTime); + /********* + ** Update events + *********/ + GameEvents.InvokeUpdateTick(this.Monitor); + if (this.FirstUpdate) + { + GameEvents.InvokeFirstUpdateTick(this.Monitor); + this.FirstUpdate = false; + } + if (this.CurrentUpdateTick % 2 == 0) + GameEvents.InvokeSecondUpdateTick(this.Monitor); + if (this.CurrentUpdateTick % 4 == 0) + GameEvents.InvokeFourthUpdateTick(this.Monitor); + if (this.CurrentUpdateTick % 8 == 0) + GameEvents.InvokeEighthUpdateTick(this.Monitor); + if (this.CurrentUpdateTick % 15 == 0) + GameEvents.InvokeQuarterSecondTick(this.Monitor); + if (this.CurrentUpdateTick % 30 == 0) + GameEvents.InvokeHalfSecondTick(this.Monitor); + if (this.CurrentUpdateTick % 60 == 0) + GameEvents.InvokeOneSecondTick(this.Monitor); + this.CurrentUpdateTick += 1; + if (this.CurrentUpdateTick >= 60) + this.CurrentUpdateTick = 0; + + /********* + ** Update input state + *********/ + this.KStatePrior = this.KStateNow; + for (PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i); + + this.UpdateCrashTimer.Reset(); } catch (Exception ex) { - this.Monitor.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); - } + // log error + this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error); - /********* - ** Update events - *********/ - GameEvents.InvokeUpdateTick(this.Monitor); - if (this.FirstUpdate) - { - GameEvents.InvokeFirstUpdateTick(this.Monitor); - this.FirstUpdate = false; + // exit if irrecoverable + if (!this.UpdateCrashTimer.Decrement()) + this.Monitor.ExitGameImmediately("the game crashed when updating, and SMAPI was unable to recover the game."); } - if (this.CurrentUpdateTick % 2 == 0) - GameEvents.InvokeSecondUpdateTick(this.Monitor); - if (this.CurrentUpdateTick % 4 == 0) - GameEvents.InvokeFourthUpdateTick(this.Monitor); - if (this.CurrentUpdateTick % 8 == 0) - GameEvents.InvokeEighthUpdateTick(this.Monitor); - if (this.CurrentUpdateTick % 15 == 0) - GameEvents.InvokeQuarterSecondTick(this.Monitor); - if (this.CurrentUpdateTick % 30 == 0) - GameEvents.InvokeHalfSecondTick(this.Monitor); - if (this.CurrentUpdateTick % 60 == 0) - GameEvents.InvokeOneSecondTick(this.Monitor); - this.CurrentUpdateTick += 1; - if (this.CurrentUpdateTick >= 60) - this.CurrentUpdateTick = 0; - - /********* - ** Update input state - *********/ - this.KStatePrior = this.KStateNow; - for (PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i); } /// The method called to draw everything to the screen. @@ -577,7 +591,7 @@ namespace StardewModdingAPI.Framework try { this.DrawImpl(gameTime); - this.FailedDraws = 0; + this.DrawCrashTimer.Reset(); } catch (Exception ex) { @@ -585,12 +599,11 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); // exit if irrecoverable - if (this.FailedDraws >= this.MaxFailedDraws) + if (!this.DrawCrashTimer.Decrement()) { this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); return; } - this.FailedDraws++; // abort in known unrecoverable cases if (Game1.toolSpriteSheet?.IsDisposed == true) diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 61b97baa..60171493 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -121,6 +121,7 @@ + -- cgit From d5ea812d7c82eac88b88c8f41756972a5322add6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 11:51:49 -0400 Subject: adjust return-to-title code --- src/StardewModdingAPI/Framework/SGame.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index cb7c0e7b..b281ef30 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework private int AfterLoadTimer = 5; /// Whether the game is returning to the menu. - private bool IsExiting; + private bool IsExitingToTitle; /// Whether the game is saving and SMAPI has already raised . private bool IsBetweenSaveEvents; @@ -337,17 +337,16 @@ namespace StardewModdingAPI.Framework *********/ // before exit to title if (Game1.exitToTitle) - this.IsExiting = true; + this.IsExitingToTitle = true; // after exit to title - if (Context.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) + if (Context.IsWorldReady && this.IsExitingToTitle && Game1.activeClickableMenu is TitleMenu) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); - Context.IsWorldReady = false; + this.IsExitingToTitle = false; + this.CleanupAfterReturnToTitle(); SaveEvents.InvokeAfterReturnToTitle(this.Monitor); - this.AfterLoadTimer = 5; - this.IsExiting = false; } /********* @@ -1290,6 +1289,14 @@ namespace StardewModdingAPI.Framework /**** ** Methods ****/ + /// Perform any cleanup needed when the player unloads a save and returns to the title screen. + private void CleanupAfterReturnToTitle() + { + Context.IsWorldReady = false; + this.AfterLoadTimer = 5; + this.PreviousSaveID = 0; + } + /// Get the controller buttons which are currently pressed. /// The controller to check. private Buttons[] GetButtonsDown(PlayerIndex index) -- cgit From 6ce07be2fa1105e575b8f2bf263492ee9d411fcb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 11:52:21 -0400 Subject: fix crash in unreleased code when loading a save that hasn't visited the mines yet --- src/StardewModdingAPI/Framework/SGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index b281ef30..7f08e7b9 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -512,7 +512,7 @@ namespace StardewModdingAPI.Framework this.PreviousDay = Game1.dayOfMonth; this.PreviousSeason = Game1.currentSeason; this.PreviousYear = Game1.year; - this.PreviousMineLevel = Game1.mine.mineLevel; + this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; this.PreviousSaveID = Game1.uniqueIDForThisGame; } -- cgit From f9c708d863816fea6c666d13e964240f745d203c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 12:16:25 -0400 Subject: drop support for multiple player indexes (not relevant for Stardew Valley) --- src/StardewModdingAPI/Events/ControlEvents.cs | 20 ++-- src/StardewModdingAPI/Framework/SGame.cs | 137 ++++++++++++-------------- 2 files changed, 72 insertions(+), 85 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/StardewModdingAPI/Events/ControlEvents.cs index 790bf193..80d0f547 100644 --- a/src/StardewModdingAPI/Events/ControlEvents.cs +++ b/src/StardewModdingAPI/Events/ControlEvents.cs @@ -77,40 +77,36 @@ namespace StardewModdingAPI.Events /// Raise a event. /// Encapsulates monitoring and logging. - /// The player who pressed the button. /// The controller button that was pressed. - internal static void InvokeButtonPressed(IMonitor monitor, PlayerIndex playerIndex, Buttons button) + internal static void InvokeButtonPressed(IMonitor monitor, Buttons button) { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(playerIndex, button)); + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonPressed)}", ControlEvents.ControllerButtonPressed?.GetInvocationList(), null, new EventArgsControllerButtonPressed(PlayerIndex.One, button)); } /// Raise a event. /// Encapsulates monitoring and logging. - /// The player who released the button. /// The controller button that was released. - internal static void InvokeButtonReleased(IMonitor monitor, PlayerIndex playerIndex, Buttons button) + internal static void InvokeButtonReleased(IMonitor monitor, Buttons button) { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(playerIndex, button)); + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerButtonReleased)}", ControlEvents.ControllerButtonReleased?.GetInvocationList(), null, new EventArgsControllerButtonReleased(PlayerIndex.One, button)); } /// Raise a event. /// Encapsulates monitoring and logging. - /// The player who pressed the trigger button. /// The trigger button that was pressed. /// The current trigger value. - internal static void InvokeTriggerPressed(IMonitor monitor, PlayerIndex playerIndex, Buttons button, float value) + internal static void InvokeTriggerPressed(IMonitor monitor, Buttons button, float value) { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(playerIndex, button, value)); + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerPressed)}", ControlEvents.ControllerTriggerPressed?.GetInvocationList(), null, new EventArgsControllerTriggerPressed(PlayerIndex.One, button, value)); } /// Raise a event. /// Encapsulates monitoring and logging. - /// The player who pressed the trigger button. /// The trigger button that was pressed. /// The current trigger value. - internal static void InvokeTriggerReleased(IMonitor monitor, PlayerIndex playerIndex, Buttons button, float value) + internal static void InvokeTriggerReleased(IMonitor monitor, Buttons button, float value) { - monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(playerIndex, button, value)); + monitor.SafelyRaiseGenericEvent($"{nameof(ControlEvents)}.{nameof(ControlEvents.ControllerTriggerReleased)}", ControlEvents.ControllerTriggerReleased?.GetInvocationList(), null, new EventArgsControllerTriggerReleased(PlayerIndex.One, button, value)); } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 7f08e7b9..36722339 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Framework ** Game state ****/ /// Arrays of pressed controller buttons indexed by . - private readonly Buttons[][] PreviouslyPressedButtons = { new Buttons[0], new Buttons[0], new Buttons[0], new Buttons[0] }; + private Buttons[] PreviouslyPressedButtons = new Buttons[0]; /// A record of the keyboard state (i.e. the up/down state for each button) as of the latest tick. private KeyboardState KStateNow; @@ -359,36 +359,35 @@ namespace StardewModdingAPI.Framework this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); // raise key pressed - foreach (var key in this.FramePressedKeys) + foreach (Keys key in this.FramePressedKeys) ControlEvents.InvokeKeyPressed(this.Monitor, key); // raise key released - foreach (var key in this.FrameReleasedKeys) + foreach (Keys key in this.FrameReleasedKeys) ControlEvents.InvokeKeyReleased(this.Monitor, key); // raise controller button pressed - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + foreach (Buttons button in this.GetFramePressedButtons()) { - var buttons = this.GetFramePressedButtons(i); - foreach (var button in buttons) + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonPressed(this.Monitor, i, button); + var triggers = GamePad.GetState(PlayerIndex.One).Triggers; + ControlEvents.InvokeTriggerPressed(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right); } + else + ControlEvents.InvokeButtonPressed(this.Monitor, button); } // raise controller button released - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + foreach (Buttons button in this.GetFrameReleasedButtons()) { - foreach (var button in this.GetFrameReleasedButtons(i)) + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonReleased(this.Monitor, i, button); + var triggers = GamePad.GetState(PlayerIndex.One).Triggers; + ControlEvents.InvokeTriggerReleased(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right); } + else + ControlEvents.InvokeButtonReleased(this.Monitor, button); } // raise keyboard state changed @@ -566,8 +565,7 @@ namespace StardewModdingAPI.Framework ** Update input state *********/ this.KStatePrior = this.KStateNow; - for (PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i); + this.PreviouslyPressedButtons = this.GetButtonsDown(); this.UpdateCrashTimer.Reset(); } @@ -1298,10 +1296,9 @@ namespace StardewModdingAPI.Framework } /// Get the controller buttons which are currently pressed. - /// The controller to check. - private Buttons[] GetButtonsDown(PlayerIndex index) + private Buttons[] GetButtonsDown() { - var state = GamePad.GetState(index); + var state = GamePad.GetState(PlayerIndex.One); var buttons = new List(); if (state.IsConnected) { @@ -1327,59 +1324,57 @@ namespace StardewModdingAPI.Framework } /// Get the controller buttons which were pressed after the last update. - /// The controller to check. - private Buttons[] GetFramePressedButtons(PlayerIndex index) + private Buttons[] GetFramePressedButtons() { - var state = GamePad.GetState(index); + var state = GamePad.GetState(PlayerIndex.One); var buttons = new List(); if (state.IsConnected) { - if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A); - if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B); - if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back); - if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton); - if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder); - if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick); - if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder); - if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick); - if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start); - if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X); - if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y); - if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp); - if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown); - if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft); - if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight); - if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger); - if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger); + if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A); + if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B); + if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back); + if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton); + if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder); + if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick); + if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder); + if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick); + if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start); + if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X); + if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y); + if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp); + if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown); + if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft); + if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight); + if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger); + if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger); } return buttons.ToArray(); } /// Get the controller buttons which were released after the last update. - /// The controller to check. - private Buttons[] GetFrameReleasedButtons(PlayerIndex index) + private Buttons[] GetFrameReleasedButtons() { - var state = GamePad.GetState(index); + var state = GamePad.GetState(PlayerIndex.One); var buttons = new List(); if (state.IsConnected) { - if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A, index)) buttons.Add(Buttons.A); - if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B, index)) buttons.Add(Buttons.B); - if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back, index)) buttons.Add(Buttons.Back); - if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton, index)) buttons.Add(Buttons.BigButton); - if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder, index)) buttons.Add(Buttons.LeftShoulder); - if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick, index)) buttons.Add(Buttons.LeftStick); - if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder, index)) buttons.Add(Buttons.RightShoulder); - if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick, index)) buttons.Add(Buttons.RightStick); - if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start, index)) buttons.Add(Buttons.Start); - if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X, index)) buttons.Add(Buttons.X); - if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y, index)) buttons.Add(Buttons.Y); - if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up, index)) buttons.Add(Buttons.DPadUp); - if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down, index)) buttons.Add(Buttons.DPadDown); - if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left, index)) buttons.Add(Buttons.DPadLeft); - if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right, index)) buttons.Add(Buttons.DPadRight); - if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left, index)) buttons.Add(Buttons.LeftTrigger); - if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right, index)) buttons.Add(Buttons.RightTrigger); + if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A); + if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B); + if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back); + if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton); + if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder); + if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick); + if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder); + if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick); + if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start); + if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X); + if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y); + if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp); + if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown); + if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft); + if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight); + if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger); + if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger); } return buttons.ToArray(); } @@ -1387,37 +1382,33 @@ namespace StardewModdingAPI.Framework /// Get whether a controller button was pressed since the last check. /// The controller button to check. /// The last known state. - /// The player whose controller to check. - private bool WasButtonJustPressed(Buttons button, ButtonState buttonState, PlayerIndex stateIndex) + private bool WasButtonJustPressed(Buttons button, ButtonState buttonState) { - return buttonState == ButtonState.Pressed && !this.PreviouslyPressedButtons[(int)stateIndex].Contains(button); + return buttonState == ButtonState.Pressed && !this.PreviouslyPressedButtons.Contains(button); } /// Get whether a controller button was released since the last check. /// The controller button to check. /// The last known state. - /// The player whose controller to check. - private bool WasButtonJustReleased(Buttons button, ButtonState buttonState, PlayerIndex stateIndex) + private bool WasButtonJustReleased(Buttons button, ButtonState buttonState) { - return buttonState == ButtonState.Released && this.PreviouslyPressedButtons[(int)stateIndex].Contains(button); + return buttonState == ButtonState.Released && this.PreviouslyPressedButtons.Contains(button); } /// Get whether an analogue controller button was pressed since the last check. /// The controller button to check. /// The last known value. - /// The player whose controller to check. - private bool WasButtonJustPressed(Buttons button, float value, PlayerIndex stateIndex) + private bool WasButtonJustPressed(Buttons button, float value) { - return this.WasButtonJustPressed(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released, stateIndex); + return this.WasButtonJustPressed(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released); } /// Get whether an analogue controller button was released since the last check. /// The controller button to check. /// The last known value. - /// The player whose controller to check. - private bool WasButtonJustReleased(Buttons button, float value, PlayerIndex stateIndex) + private bool WasButtonJustReleased(Buttons button, float value) { - return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released, stateIndex); + return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released); } /// Get the player inventory changes between two states. -- cgit From 228f84eea60e9a2c0eb6abd167ae173a7332974d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 14:30:57 -0400 Subject: raise location/player changed events on load --- src/StardewModdingAPI/Framework/SGame.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 36722339..87f46523 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -438,25 +438,25 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { - // raise events (only when something changes, not on the initial load) + // raise current location changed + if (Game1.currentLocation != this.PreviousGameLocation) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); + LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + } + + // raise player changed + if (Game1.player != this.PreviousFarmer) + PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); + + // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { // raise location list changed if (this.GetHash(Game1.locations) != this.PreviousGameLocations) LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - // raise current location changed - if (Game1.currentLocation != this.PreviousGameLocation) - { - if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); - } - - // raise player changed - if (Game1.player != this.PreviousFarmer) - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); - // raise player leveled up a skill if (Game1.player.combatLevel != this.PreviousCombatLevel) PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); -- cgit From bc1e17bc8c0ab23420e000d06a99289bbfe9b530 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 15:44:18 -0400 Subject: fix mod-loading code not accounting for metadata failure --- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 10 ++++++++++ src/StardewModdingAPI/Program.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 53c28ace..2c68a639 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -131,9 +131,19 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mods to process. public IEnumerable ProcessDependencies(IEnumerable mods) { + // initialise metadata mods = mods.ToArray(); var sortedMods = new Stack(); var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); + + // handle failed mods + foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed)) + { + states[mod] = ModDependencyStatus.Failed; + sortedMods.Push(mod); + } + + // sort mods foreach (IModMetadata mod in mods) this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 7a8189bd..3a7cb9ce 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -322,7 +322,7 @@ namespace StardewModdingAPI // check for deprecated metadata IList deprecationWarnings = new List(); - foreach (IModMetadata mod in mods) + foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) { // missing fields that will be required in SMAPI 2.0 { -- cgit From 37a2e345074449b02d173c5e9a6984f2f6e11789 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 May 2017 21:25:18 -0400 Subject: also raise location-list-changed on load --- src/StardewModdingAPI/Framework/SGame.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 87f46523..3d421a37 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -446,6 +446,10 @@ namespace StardewModdingAPI.Framework LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); } + // raise location list changed + if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + // raise player changed if (Game1.player != this.PreviousFarmer) PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); @@ -453,10 +457,6 @@ namespace StardewModdingAPI.Framework // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { - // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - // raise player leveled up a skill if (Game1.player.combatLevel != this.PreviousCombatLevel) PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); -- cgit