diff options
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Constants.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Enums/LoadStage.cs | 31 | ||||
-rw-r--r-- | src/SMAPI/Framework/IModMetadata.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/LogManager.cs | 23 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 51 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadata.cs | 19 | ||||
-rw-r--r-- | src/SMAPI/Framework/Patching/GamePatcher.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/Patching/IHarmonyPatch.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Patches/LoadContextPatch.cs | 73 |
10 files changed, 164 insertions, 79 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 54fb54ab..a81a6bc9 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -54,7 +54,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.2"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.3"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); diff --git a/src/SMAPI/Enums/LoadStage.cs b/src/SMAPI/Enums/LoadStage.cs index 5c2b0412..302c263b 100644 --- a/src/SMAPI/Enums/LoadStage.cs +++ b/src/SMAPI/Enums/LoadStage.cs @@ -4,33 +4,42 @@ namespace StardewModdingAPI.Enums public enum LoadStage { /// <summary>A save is not loaded or loading.</summary> - None, + None = 0, /// <summary>The game is creating a new save slot, and has initialized the basic save info.</summary> - CreatedBasicInfo, + CreatedBasicInfo = 1, + + /// <summary>The game is creating a new save slot, and has added the location instances but hasn't fully initialized them yet.</summary> + CreatedInitialLocations = 10, /// <summary>The game is creating a new save slot, and has initialized the in-game locations.</summary> - CreatedLocations, + CreatedLocations = 2, /// <summary>The game is creating a new save slot, and has created the physical save files.</summary> - CreatedSaveFile, + CreatedSaveFile = 3, /// <summary>The game is loading a save slot, and has read the raw save data into <see cref="StardewValley.SaveGame.loaded"/>. Not applicable when connecting to a multiplayer host. This is equivalent to <see cref="StardewValley.SaveGame.getLoadEnumerator"/> value 20.</summary> - SaveParsed, + SaveParsed = 4, /// <summary>The game is loading a save slot, and has applied the basic save info (including player data). Not applicable when connecting to a multiplayer host. Note that some basic info (like daily luck) is not initialized at this point. This is equivalent to <see cref="StardewValley.SaveGame.getLoadEnumerator"/> value 36.</summary> - SaveLoadedBasicInfo, + SaveLoadedBasicInfo = 5, + + /// <summary>The game is loading a save slot and has added the location instances, but hasn't restored their save data yet. Not applicable when connecting to a multiplayer host.</summary> + SaveAddedLocations = 11, - /// <summary>The game is loading a save slot, and has applied the in-game location data. Not applicable when connecting to a multiplayer host. This is equivalent to <see cref="StardewValley.SaveGame.getLoadEnumerator"/> value 50.</summary> - SaveLoadedLocations, + /// <summary>The game is loading a save slot, and has restored the in-game location data. Not applicable when connecting to a multiplayer host. This is equivalent to <see cref="StardewValley.SaveGame.getLoadEnumerator"/> value 50.</summary> + SaveLoadedLocations = 6, /// <summary>The final metadata has been loaded from the save file. This happens before the game applies problem fixes, checks for achievements, starts music, etc. Not applicable when connecting to a multiplayer host.</summary> - Preloaded, + Preloaded = 7, /// <summary>The save is fully loaded, but the world may not be fully initialized yet.</summary> - Loaded, + Loaded = 8, /// <summary>The save is fully loaded, the world has been initialized, and <see cref="Context.IsWorldReady"/> is now true.</summary> - Ready + Ready = 9, + + /// <summary>The game is exiting the loaded save and returning to the title screen. This happens before it returns to title; see <see cref="None"/> after it returns.</summary> + ReturningToTitle = 12 } } diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 70cf0036..5d2f352d 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Framework /// <summary>The reason the mod failed to load, if applicable.</summary> ModFailReason? FailReason { get; } - /// <summary>Indicates non-error issues with the mod.</summary> + /// <summary>The non-error issues with the mod, ignoring those suppressed via <see cref="DataRecord"/>.</summary> ModWarning Warnings { get; } /// <summary>The reason the metadata is invalid, if any.</summary> @@ -124,9 +124,9 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the mod has at least one valid update key set.</summary> bool HasValidUpdateKeys(); - /// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="DataRecord"/>.</summary> + /// <summary>Get whether the mod has any of the given warnings, ignoring those suppressed via <see cref="DataRecord"/>.</summary> /// <param name="warnings">The warnings to check.</param> - bool HasUnsuppressedWarnings(params ModWarning[] warnings); + bool HasWarnings(params ModWarning[] warnings); /// <summary>Get a relative path which includes the root folder name.</summary> string GetRelativePathWithRoot(); diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 2c7be399..0dd45355 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -252,11 +252,22 @@ namespace StardewModdingAPI.Framework.Logging break; // missing content folder exception - case FileNotFoundException ex when ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.": // path in error is hardcoded regardless of install path + case FileNotFoundException ex when ex.Message == "Couldn't find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.": // path in error is hardcoded regardless of install path this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error); this.Monitor.Log($"Technical details: {ex.GetLogSummary()}"); break; + // path too long exception + case PathTooLongException: + { + string[] affectedPaths = PathUtilities.GetTooLongPaths(Constants.ModsPath).ToArray(); + string message = affectedPaths.Any() + ? $"SMAPI can't launch because some of your mod files exceed the maximum path length on {Constants.Platform}.\nIf you need help fixing this error, see https://smapi.io/help\n\nAffected paths:\n {string.Join("\n ", affectedPaths)}" + : $"The game failed to launch: {exception.GetLogSummary()}"; + this.MonitorForGame.Log(message, LogLevel.Error); + } + break; + // generic exception default: this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error); @@ -505,7 +516,7 @@ namespace StardewModdingAPI.Framework.Logging { this.LogModWarningGroup( modsWithWarnings, - match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), + match: mod => mod.HasWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), level: LogLevel.Debug, heading: "Direct system access", blurb: new[] @@ -517,11 +528,11 @@ namespace StardewModdingAPI.Framework.Logging modLabel: mod => { List<string> labels = new List<string>(); - if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole)) + if (mod.HasWarnings(ModWarning.AccessesConsole)) labels.Add("console"); - if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem)) + if (mod.HasWarnings(ModWarning.AccessesFilesystem)) labels.Add("files"); - if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell)) + if (mod.HasWarnings(ModWarning.AccessesShell)) labels.Add("shells/processes"); return $"{mod.DisplayName} ({string.Join(", ", labels)})"; @@ -582,7 +593,7 @@ namespace StardewModdingAPI.Framework.Logging /// <param name="blurb">A detailed explanation of the warning, split into lines.</param> private void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb) { - this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb); + this.LogModWarningGroup(mods, mod => mod.HasWarnings(warning), level, heading, blurb); } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 4fae0f44..69535aa5 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -276,37 +276,40 @@ namespace StardewModdingAPI.Framework.ModLoading // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; - for (int i = 0; i < module.AssemblyReferences.Count; i++) + if (this.RewriteMods) { - // remove old assembly reference - if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + for (int i = 0; i < module.AssemblyReferences.Count; i++) { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); - platformChanged = true; - module.AssemblyReferences.RemoveAt(i); - i--; + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} 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); + 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<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); - foreach (TypeReference type in typeReferences) - this.ChangeTypeScope(type); + // rewrite type scopes to use target assemblies + IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); - // rewrite types using custom attributes - foreach (TypeDefinition type in module.GetTypes()) - { - foreach (var attr in type.CustomAttributes) + // rewrite types using custom attributes + foreach (TypeDefinition type in module.GetTypes()) { - foreach (var conField in attr.ConstructorArguments) + foreach (var attr in type.CustomAttributes) { - if (conField.Value is TypeReference typeRef) - this.ChangeTypeScope(typeRef); + foreach (var conField in attr.ConstructorArguments) + { + if (conField.Value is TypeReference typeRef) + this.ChangeTypeScope(typeRef); + } } } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 18d2b112..b4de3d6c 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -14,6 +14,13 @@ namespace StardewModdingAPI.Framework.ModLoading internal class ModMetadata : IModMetadata { /********* + ** Fields + *********/ + /// <summary>The non-error issues with the mod, including warnings suppressed by the data record.</summary> + private ModWarning ActualWarnings = ModWarning.None; + + + /********* ** Accessors *********/ /// <inheritdoc /> @@ -41,7 +48,7 @@ namespace StardewModdingAPI.Framework.ModLoading public ModFailReason? FailReason { get; private set; } /// <inheritdoc /> - public ModWarning Warnings { get; private set; } + public ModWarning Warnings => this.ActualWarnings & ~(this.DataRecord?.DataRecord.SuppressWarnings ?? ModWarning.None); /// <inheritdoc /> public string Error { get; private set; } @@ -116,7 +123,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <inheritdoc /> public IModMetadata SetWarning(ModWarning warning) { - this.Warnings |= warning; + this.ActualWarnings |= warning; return this; } @@ -218,12 +225,10 @@ namespace StardewModdingAPI.Framework.ModLoading } /// <inheritdoc /> - public bool HasUnsuppressedWarnings(params ModWarning[] warnings) + public bool HasWarnings(params ModWarning[] warnings) { - return warnings.Any(warning => - this.Warnings.HasFlag(warning) - && (this.DataRecord?.DataRecord == null || !this.DataRecord.DataRecord.SuppressWarnings.HasFlag(warning)) - ); + ModWarning curWarnings = this.Warnings; + return warnings.Any(warning => curWarnings.HasFlag(warning)); } /// <inheritdoc /> diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs index 82d7b9c8..ddecda08 100644 --- a/src/SMAPI/Framework/Patching/GamePatcher.cs +++ b/src/SMAPI/Framework/Patching/GamePatcher.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Patching } catch (Exception ex) { - this.Monitor.Log($"Couldn't apply runtime patch '{patch.Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); + this.Monitor.Log($"Couldn't apply runtime patch '{patch.GetType().Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); this.Monitor.Log(ex.GetLogSummary(), LogLevel.Trace); } } diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs index 922243fa..38d30ab2 100644 --- a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs +++ b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs @@ -9,9 +9,9 @@ namespace StardewModdingAPI.Framework.Patching /// <summary>A Harmony patch to apply.</summary> internal interface IHarmonyPatch { - /// <summary>A unique name for this patch.</summary> - string Name { get; } - + /********* + ** Methods + *********/ /// <summary>Apply the Harmony patch.</summary> /// <param name="harmony">The Harmony instance.</param> #if HARMONY_2 diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2d783eb2..5df4b61b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1054,17 +1054,24 @@ namespace StardewModdingAPI.Framework LoadStage oldStage = Context.LoadStage; Context.LoadStage = newStage; this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); - if (newStage == LoadStage.None) - { - this.Monitor.Log("Context: returned to title"); - this.OnReturnedToTitle(); - } - // override chatbox - if (newStage == LoadStage.Loaded) + // handle stages + switch (newStage) { - Game1.onScreenMenus.Remove(Game1.chatBox); - Game1.onScreenMenus.Add(Game1.chatBox = new SChatBox(this.LogManager.MonitorForGame)); + case LoadStage.ReturningToTitle: + this.Monitor.Log("Context: returning to title"); + this.OnReturningToTitle(); + break; + + case LoadStage.None: + this.JustReturnedToTitle = true; + break; + + case LoadStage.Loaded: + // override chatbox + Game1.onScreenMenus.Remove(Game1.chatBox); + Game1.onScreenMenus.Add(Game1.chatBox = new SChatBox(this.LogManager.MonitorForGame)); + break; } // raise events @@ -1113,13 +1120,12 @@ namespace StardewModdingAPI.Framework this.EventManager.DayEnding.RaiseEmpty(); } - /// <summary>Raised after the player returns to the title screen.</summary> - private void OnReturnedToTitle() + /// <summary>Raised immediately before the player returns to the title screen.</summary> + private void OnReturningToTitle() { // perform cleanup this.Multiplayer.CleanupOnMultiplayerExit(); this.ContentCore.OnReturningToTitleScreen(); - this.JustReturnedToTitle = true; } /// <summary>Raised before the game exits.</summary> diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index ceda061b..c43d7071 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -29,12 +29,8 @@ namespace StardewModdingAPI.Patches /// <summary>A callback to invoke when the load stage changes.</summary> private static Action<LoadStage> OnStageChanged; - - /********* - ** Accessors - *********/ - /// <inheritdoc /> - public string Name => nameof(LoadContextPatch); + /// <summary>Whether the game is running running the code in <see cref="Game1.loadForNewGame"/>.</summary> + private static bool IsInLoadForNewGame; /********* @@ -62,11 +58,24 @@ namespace StardewModdingAPI.Patches prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_TitleMenu_CreatedNewCharacter)) ); - // detect CreatedLocations + // detect CreatedInitialLocations and SaveAddedLocations + harmony.Patch( + original: AccessTools.Method(typeof(Game1), nameof(Game1.AddModNPCs)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_AddModNPCs)) + ); + + // detect CreatedLocations, and track IsInLoadForNewGame harmony.Patch( original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_LoadForNewGame)), postfix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.After_Game1_LoadForNewGame)) ); + + // detect ReturningToTitle + harmony.Patch( + original: AccessTools.Method(typeof(Game1), nameof(Game1.CleanupReturningToTitle)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Before_Game1_CleanupReturningToTitle)) + ); } @@ -82,16 +91,58 @@ namespace StardewModdingAPI.Patches return true; } + /// <summary>Called before <see cref="Game1.AddModNPCs"/>.</summary> + /// <returns>Returns whether to execute the original method.</returns> + /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> + private static bool Before_Game1_AddModNPCs() + { + // When this method is called from Game1.loadForNewGame, it happens right after adding the vanilla + // locations but before initializing them. + if (LoadContextPatch.IsInLoadForNewGame) + { + LoadContextPatch.OnStageChanged(LoadContextPatch.IsCreating() + ? LoadStage.CreatedInitialLocations + : LoadStage.SaveAddedLocations + ); + } + + return true; + } + + /// <summary>Called before <see cref="Game1.CleanupReturningToTitle"/>.</summary> + /// <returns>Returns whether to execute the original method.</returns> + /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> + private static bool Before_Game1_CleanupReturningToTitle() + { + LoadContextPatch.OnStageChanged(LoadStage.ReturningToTitle); + return true; + } + + /// <summary>Called before <see cref="Game1.loadForNewGame"/>.</summary> + /// <returns>Returns whether to execute the original method.</returns> + /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> + private static bool Before_Game1_LoadForNewGame() + { + LoadContextPatch.IsInLoadForNewGame = true; + return true; + } + /// <summary>Called after <see cref="Game1.loadForNewGame"/>.</summary> /// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks> private static void After_Game1_LoadForNewGame() { - bool creating = - (Game1.currentMinigame is Intro) // creating save with intro - || (Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField<bool>(menu, "transitioningCharacterCreationMenu").GetValue()); // creating save, skipped intro + LoadContextPatch.IsInLoadForNewGame = false; - if (creating) + if (LoadContextPatch.IsCreating()) LoadContextPatch.OnStageChanged(LoadStage.CreatedLocations); } + + /// <summary>Get whether the save file is currently being created.</summary> + private static bool IsCreating() + { + return + (Game1.currentMinigame is Intro) // creating save with intro + || (Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField<bool>(menu, "transitioningCharacterCreationMenu").GetValue()); // creating save, skipped intro + } } } |