From 8a475b35790506a18aa94a68530b40e8326017ca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:29 -0500 Subject: move error-handling Harmony patches into a new Error Handler bundled mod --- src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 73 ++++++++ .../Patches/DialogueErrorPatch.cs | 191 +++++++++++++++++++++ .../Patches/EventErrorPatch.cs | 114 ++++++++++++ .../Patches/LoadErrorPatch.cs | 157 +++++++++++++++++ .../Patches/ObjectErrorPatch.cs | 143 +++++++++++++++ .../Patches/ScheduleErrorPatch.cs | 115 +++++++++++++ .../SMAPI.Mods.ErrorHandler.csproj | 46 +++++ src/SMAPI.Mods.ErrorHandler/i18n/de.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/default.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/es.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/fr.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/hu.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/it.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/ja.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/ko.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/pt.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/ru.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/tr.json | 4 + src/SMAPI.Mods.ErrorHandler/i18n/zh.json | 4 + src/SMAPI.Mods.ErrorHandler/manifest.json | 9 + 20 files changed, 896 insertions(+) create mode 100644 src/SMAPI.Mods.ErrorHandler/ModEntry.cs create mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs create mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs create mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs create mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs create mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs create mode 100644 src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/de.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/default.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/es.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/fr.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/hu.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/it.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/ja.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/ko.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/pt.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/ru.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/tr.json create mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/zh.json create mode 100644 src/SMAPI.Mods.ErrorHandler/manifest.json (limited to 'src/SMAPI.Mods.ErrorHandler') diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs new file mode 100644 index 00000000..fa9338f4 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Mods.ErrorHandler.Patches; +using StardewValley; + +namespace StardewModdingAPI.Mods.ErrorHandler +{ + /// The main entry point for the mod. + public class ModEntry : Mod + { + /********* + ** Private methods + *********/ + /// Whether custom content was removed from the save data to avoid a crash. + private bool IsSaveContentRemoved; + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public override void Entry(IModHelper helper) + { + // get SMAPI core types + SCore core = SCore.Instance; + LogManager logManager = core.GetType().GetField("LogManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(core) as LogManager; + if (logManager == null) + { + this.Monitor.Log($"Can't access SMAPI's internal log manager. Error-handling patches won't be applied.", LogLevel.Error); + return; + } + + // apply patches + new GamePatcher(this.Monitor).Apply( + new EventErrorPatch(logManager.MonitorForGame), + new DialogueErrorPatch(logManager.MonitorForGame, this.Helper.Reflection), + new ObjectErrorPatch(), + new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved), + new ScheduleErrorPatch(logManager.MonitorForGame) + ); + + // hook events + this.Helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded; + } + + + /********* + ** Private methods + *********/ + /// Raised after custom content is removed from the save data to avoid a crash. + internal void OnSaveContentRemoved() + { + this.IsSaveContentRemoved = true; + } + + /// The method invoked when a save is loaded. + /// The event sender. + /// The event arguments. + public void OnSaveLoaded(object sender, SaveLoadedEventArgs e) + { + // show in-game warning for removed save content + if (this.IsSaveContentRemoved) + { + this.IsSaveContentRemoved = false; + Game1.addHUDMessage(new HUDMessage(this.Helper.Translation.Get("warn.invalid-content-removed"), HUDMessage.error_type)); + } + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs new file mode 100644 index 00000000..ba0ca582 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/DialogueErrorPatch.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +#if HARMONY_2 +using HarmonyLib; +using StardewModdingAPI.Framework; +#else +using System.Reflection; +using Harmony; +#endif + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// A Harmony patch for the constructor which intercepts invalid dialogue lines and logs an error instead of crashing. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class DialogueErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + /// Simplifies access to private code. + private static IReflectionHelper Reflection; + + + /********* + ** Accessors + *********/ + /// + public string Name => nameof(DialogueErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + /// Simplifies access to private code. + public DialogueErrorPatch(IMonitor monitorForGame, IReflectionHelper reflector) + { + DialogueErrorPatch.MonitorForGame = monitorForGame; + DialogueErrorPatch.Reflection = reflector; + } + + + /// +#if HARMONY_2 + public void Apply(Harmony harmony) + { + harmony.Patch( + original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), + finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_Dialogue_Constructor)) + ); + harmony.Patch( + original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, + finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_NPC_CurrentDialogue)) + ); + } +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_Dialogue_Constructor)) + ); + harmony.Patch( + original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue)) + ); + } +#endif + + + /********* + ** Private methods + *********/ +#if HARMONY_2 + /// The method to call after the Dialogue constructor. + /// The instance being patched. + /// The dialogue being parsed. + /// The NPC for which the dialogue is being parsed. + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception) + { + if (__exception != null) + { + // log message + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error); + + // set default dialogue + IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return null; + } + + /// The method to call after . + /// The instance being patched. + /// The return value of the original method. + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_NPC_CurrentDialogue(NPC __instance, ref Stack __result, Exception __exception) + { + if (__exception == null) + return null; + + DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{__exception.GetLogSummary()}", LogLevel.Error); + __result = new Stack(); + + return null; + } +#else + + /// The method to call instead of the Dialogue constructor. + /// The instance being patched. + /// The dialogue being parsed. + /// The NPC for which the dialogue is being parsed. + /// Returns whether to execute the original method. + private static bool Before_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker) + { + // get private members + bool nameArraysTranslated = DialogueErrorPatch.Reflection.GetField(typeof(Dialogue), "nameArraysTranslated").GetValue(); + IReflectedMethod translateArraysOfStrings = DialogueErrorPatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); + IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + + // replicate base constructor + __instance.dialogues ??= new List(); + + // duplicate code with try..catch + try + { + if (!nameArraysTranslated) + translateArraysOfStrings.Invoke(); + __instance.speaker = speaker; + parseDialogueString.Invoke(masterDialogue); + checkForSpecialDialogueAttributes.Invoke(); + } + catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) + { + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); + + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return false; + } + + /// The method to call instead of . + /// The instance being patched. + /// The return value of the original method. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack __result, MethodInfo __originalMethod) + { + const string key = nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Stack)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) + { + DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Stack(); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs new file mode 100644 index 00000000..fabc6cad --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.CodeAnalysis; +#if HARMONY_2 +using System; +using HarmonyLib; +#else +using System.Reflection; +using Harmony; +#endif +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// A Harmony patch for which intercepts invalid preconditions and logs an error instead of crashing. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class EventErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + + /********* + ** Accessors + *********/ + /// + public string Name => nameof(EventErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + public EventErrorPatch(IMonitor monitorForGame) + { + EventErrorPatch.MonitorForGame = monitorForGame; + } + + /// +#if HARMONY_2 + public void Apply(Harmony harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Finalize_GameLocation_CheckEventPrecondition)) + ); + } +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition)) + ); + } +#endif + + + /********* + ** Private methods + *********/ +#if HARMONY_2 + /// The method to call instead of GameLocation.checkEventPrecondition. + /// The return value of the original method. + /// The precondition to be parsed. + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_GameLocation_CheckEventPrecondition(ref int __result, string precondition, Exception __exception) + { + if (__exception != null) + { + __result = -1; + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{__exception.InnerException}", LogLevel.Error); + } + + return null; + } +#else + /// The method to call instead of GameLocation.checkEventPrecondition. + /// The instance being patched. + /// The return value of the original method. + /// The precondition to be parsed. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) + { + const string key = nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); + return false; + } + catch (TargetInvocationException ex) + { + __result = -1; + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs new file mode 100644 index 00000000..2227ea07 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/LoadErrorPatch.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if HARMONY_2 +using HarmonyLib; +#else +using Harmony; +#endif +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// A Harmony patch for which prevents some errors due to broken save data. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class LoadErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file. + private static IMonitor Monitor; + + /// A callback invoked when custom content is removed from the save data to avoid a crash. + private static Action OnContentRemoved; + + + /********* + ** Accessors + *********/ + /// + public string Name => nameof(LoadErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file. + /// A callback invoked when custom content is removed from the save data to avoid a crash. + public LoadErrorPatch(IMonitor monitor, Action onContentRemoved) + { + LoadErrorPatch.Monitor = monitor; + LoadErrorPatch.OnContentRemoved = onContentRemoved; + } + + + /// +#if HARMONY_2 + public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif + { + harmony.Patch( + original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadErrorPatch.Before_SaveGame_LoadDataToLocations)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// The game locations being loaded. + /// Returns whether to execute the original method. + private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) + { + bool removedAny = + LoadErrorPatch.RemoveBrokenBuildings(gamelocations) + | LoadErrorPatch.RemoveInvalidNpcs(gamelocations); + + if (removedAny) + LoadErrorPatch.OnContentRemoved(); + + return true; + } + + /// Remove buildings which don't exist in the game data. + /// The current game locations. + private static bool RemoveBrokenBuildings(IEnumerable locations) + { + bool removedAny = false; + + foreach (BuildableGameLocation location in locations.OfType()) + { + foreach (Building building in location.buildings.ToArray()) + { + try + { + BluePrint _ = new BluePrint(building.buildingType.Value); + } + catch (SContentLoadException) + { + LoadErrorPatch.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn); + location.buildings.Remove(building); + removedAny = true; + } + } + } + + return removedAny; + } + + /// Remove NPCs which don't exist in the game data. + /// The current game locations. + private static bool RemoveInvalidNpcs(IEnumerable locations) + { + bool removedAny = false; + + IDictionary data = Game1.content.Load>("Data\\NPCDispositions"); + foreach (GameLocation location in LoadErrorPatch.GetAllLocations(locations)) + { + foreach (NPC npc in location.characters.ToArray()) + { + if (npc.isVillager() && !data.ContainsKey(npc.Name)) + { + try + { + npc.reloadSprite(); // this won't crash for special villagers like Bouncer + } + catch + { + LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); + location.characters.Remove(npc); + removedAny = true; + } + } + } + } + + return removedAny; + } + + /// Get all locations, including building interiors. + /// The main game locations. + private static IEnumerable GetAllLocations(IEnumerable locations) + { + foreach (GameLocation location in locations) + { + yield return location; + if (location is BuildableGameLocation buildableLocation) + { + foreach (GameLocation interior in buildableLocation.buildings.Select(p => p.indoors.Value).Where(p => p != null)) + yield return interior; + } + } + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs new file mode 100644 index 00000000..70f054cd --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectErrorPatch.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using StardewValley.Menus; +using SObject = StardewValley.Object; +#if HARMONY_2 +using System; +using HarmonyLib; +#else +using System.Reflection; +using Harmony; +#endif + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// A Harmony patch for which intercepts crashes due to the item no longer existing. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class ObjectErrorPatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// + public string Name => nameof(ObjectErrorPatch); + + + /********* + ** Public methods + *********/ + /// +#if HARMONY_2 + public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif + { + // object.getDescription + harmony.Patch( + original: AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)), + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_GetDescription)) + ); + + // object.getDisplayName + harmony.Patch( + original: AccessTools.Method(typeof(SObject), "loadDisplayName"), +#if HARMONY_2 + finalizer: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Finalize_Object_loadDisplayName)) +#else + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName)) +#endif + ); + + // IClickableMenu.drawToolTip + harmony.Patch( + original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)), + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_IClickableMenu_DrawTooltip)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// The instance being patched. + /// The patched method's return value. + /// Returns whether to execute the original method. + private static bool Before_Object_GetDescription(SObject __instance, ref string __result) + { + // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables + if (!__instance.IsRecipe && __instance.bigCraftable.Value && !Game1.bigCraftablesInformation.ContainsKey(__instance.ParentSheetIndex)) + { + __result = "???"; + return false; + } + + return true; + } + +#if HARMONY_2 + /// The method to call after . + /// The patched method's return value. + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_Object_loadDisplayName(ref string __result, Exception __exception) + { + if (__exception is KeyNotFoundException) + { + __result = "???"; + return null; + } + + return __exception; + } +#else + /// The method to call instead of . + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod) + { + const string key = nameof(ObjectErrorPatch.Before_Object_loadDisplayName); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (string)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException) + { + __result = "???"; + return false; + } + catch + { + return true; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif + + /// The method to call instead of . + /// The item for which to draw a tooltip. + /// Returns whether to execute the original method. + private static bool Before_IClickableMenu_DrawTooltip(Item hoveredItem) + { + // invalid edible item cause crash when drawing tooltips + if (hoveredItem is SObject obj && obj.Edibility != -300 && !Game1.objectInformation.ContainsKey(obj.ParentSheetIndex)) + return false; + + return true; + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs new file mode 100644 index 00000000..abbd1a8f --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/ScheduleErrorPatch.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +#if HARMONY_2 +using System; +using HarmonyLib; +using StardewModdingAPI.Framework; +#else +using System.Reflection; +using Harmony; +#endif + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// A Harmony patch for which intercepts crashes due to invalid schedule data. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class ScheduleErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + + /********* + ** Accessors + *********/ + /// + public string Name => nameof(ScheduleErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + public ScheduleErrorPatch(IMonitor monitorForGame) + { + ScheduleErrorPatch.MonitorForGame = monitorForGame; + } + + /// +#if HARMONY_2 + public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif + { + harmony.Patch( + original: AccessTools.Method(typeof(NPC), nameof(NPC.parseMasterSchedule)), +#if HARMONY_2 + finalizer: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Finalize_NPC_parseMasterSchedule)) +#else + prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule)) +#endif + ); + } + + + /********* + ** Private methods + *********/ +#if HARMONY_2 + /// The method to call instead of . + /// The raw schedule data to parse. + /// The instance being patched. + /// The patched method's return value. + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, Exception __exception) + { + if (__exception != null) + { + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{__exception.GetLogSummary()}", LogLevel.Error); + __result = new Dictionary(); + } + + return null; + } +#else + /// The method to call instead of . + /// The raw schedule data to parse. + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) + { + const string key = nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); + return false; + } + catch (TargetInvocationException ex) + { + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Dictionary(); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj new file mode 100644 index 00000000..5c0cf952 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -0,0 +1,46 @@ + + + ErrorHandler + StardewModdingAPI.Mods.ErrorHandler + net45 + false + x86 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/de.json b/src/SMAPI.Mods.ErrorHandler/i18n/de.json new file mode 100644 index 00000000..1de6301c --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/de.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/default.json b/src/SMAPI.Mods.ErrorHandler/i18n/default.json new file mode 100644 index 00000000..b74dcea0 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/default.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/es.json b/src/SMAPI.Mods.ErrorHandler/i18n/es.json new file mode 100644 index 00000000..8ba10b70 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/es.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/fr.json b/src/SMAPI.Mods.ErrorHandler/i18n/fr.json new file mode 100644 index 00000000..76978526 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/fr.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/hu.json b/src/SMAPI.Mods.ErrorHandler/i18n/hu.json new file mode 100644 index 00000000..92aca7d0 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/hu.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/it.json b/src/SMAPI.Mods.ErrorHandler/i18n/it.json new file mode 100644 index 00000000..5182972e --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/it.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/ja.json b/src/SMAPI.Mods.ErrorHandler/i18n/ja.json new file mode 100644 index 00000000..559c7fbe --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/ja.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)" +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/ko.json b/src/SMAPI.Mods.ErrorHandler/i18n/ko.json new file mode 100644 index 00000000..48f05c26 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/ko.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/pt.json b/src/SMAPI.Mods.ErrorHandler/i18n/pt.json new file mode 100644 index 00000000..8ea8cec9 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/pt.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/ru.json b/src/SMAPI.Mods.ErrorHandler/i18n/ru.json new file mode 100644 index 00000000..e9c3b313 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/ru.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)" +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/tr.json b/src/SMAPI.Mods.ErrorHandler/i18n/tr.json new file mode 100644 index 00000000..a05ab152 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/tr.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/zh.json b/src/SMAPI.Mods.ErrorHandler/i18n/zh.json new file mode 100644 index 00000000..e959aa40 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/zh.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)" +} diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json new file mode 100644 index 00000000..f83e2f96 --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -0,0 +1,9 @@ +{ + "Name": "Error Handler", + "Author": "SMAPI", + "Version": "3.8.3", + "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", + "UniqueID": "SMAPI.ErrorHandler", + "EntryDll": "ErrorHandler.dll", + "MinimumApiVersion": "3.8.3" +} -- cgit From 70edec65678fc07200cd817b99f812389d1e97b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:30 -0500 Subject: add detailed message for game error when parsing an item text description --- docs/release-notes.md | 1 + src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 3 +- .../Patches/UtilityErrorPatches.cs | 96 ++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs (limited to 'src/SMAPI.Mods.ErrorHandler') diff --git a/docs/release-notes.md b/docs/release-notes.md index fabe7572..5409d9ff 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * For the Error Handler mod: * Added in SMAPI 3.9. This has vanilla error-handling that was previously added by SMAPI directly. That simplifies the core SMAPI logic, and lets players or modders disable it if needed. + * Added a detailed message for the _Input string was not in a correct format_ error when the game fails to parse an item text description. ## 3.8.4 Released 15 January 2021 for Stardew Valley 1.5.3 or later. diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index fa9338f4..2f6f1939 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -40,7 +40,8 @@ namespace StardewModdingAPI.Mods.ErrorHandler new DialogueErrorPatch(logManager.MonitorForGame, this.Helper.Reflection), new ObjectErrorPatch(), new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved), - new ScheduleErrorPatch(logManager.MonitorForGame) + new ScheduleErrorPatch(logManager.MonitorForGame), + new UtilityErrorPatches() ); // hook events diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs new file mode 100644 index 00000000..481c881e --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityErrorPatches.cs @@ -0,0 +1,96 @@ +#if HARMONY_2 +using System; +using HarmonyLib; +#else +using Harmony; +#endif +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Mods.ErrorHandler.Patches +{ + /// A Harmony patch for methods to log more detailed errors. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class UtilityErrorPatches : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// + public string Name => nameof(UtilityErrorPatches); + + + /********* + ** Public methods + *********/ + /// +#if HARMONY_2 + public void Apply(Harmony harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)), + finalizer: new HarmonyMethod(this.GetType(), nameof(UtilityErrorPatches.Finalize_Utility_GetItemFromStandardTextDescription)) + ); + } +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(Utility), nameof(Utility.getItemFromStandardTextDescription)), + prefix: new HarmonyMethod(this.GetType(), nameof(UtilityErrorPatches.Before_Utility_GetItemFromStandardTextDescription)) + ); + } +#endif + + + /********* + ** Private methods + *********/ +#if HARMONY_2 + /// The method to call instead of . + /// The item text description to parse. + /// The delimiter by which to split the text description. + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_Utility_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception) + { + return __exception != null + ? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception) + : null; + } +#else + /// The method to call instead of . + /// The return value of the original method. + /// The item text description to parse. + /// The player for which the item is being parsed. + /// The delimiter by which to split the text description. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_Utility_GetItemFromStandardTextDescription(ref Item __result, string description, Farmer who, char delimiter, MethodInfo __originalMethod) + { + const string key = nameof(UtilityErrorPatches.Before_Utility_GetItemFromStandardTextDescription); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Item)__originalMethod.Invoke(null, new object[] { description, who, delimiter }); + return false; + } + catch (TargetInvocationException ex) + { + throw new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", ex.InnerException); + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif + } +} -- cgit From ebc8cd4c7f068cb687c8786623b8b617e5295e82 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jan 2021 23:28:48 -0500 Subject: add ErrorHandler to bundled mod list --- src/SMAPI.Installer/InteractiveInstaller.cs | 3 ++- src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Mods.ErrorHandler') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 035ec382..ff74a659 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -28,7 +28,8 @@ namespace StardewModdingApi.Installer /// The mod IDs which the installer should allow as bundled mods. private readonly string[] BundledModIDs = { "SMAPI.SaveBackup", - "SMAPI.ConsoleCommands" + "SMAPI.ConsoleCommands", + "SMAPI.ErrorHandler" }; /// Get the absolute file or folder paths to remove when uninstalling SMAPI. diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index f83e2f96..3394da53 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.8.3", + "Version": "3.8.4", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.8.3" + "MinimumApiVersion": "3.8.4" } -- cgit From 733750fdc4f5d16069d95880144619c0e31e8a89 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jan 2021 21:04:48 -0500 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 24 +++++++++++++----------- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 21 insertions(+), 19 deletions(-) (limited to 'src/SMAPI.Mods.ErrorHandler') diff --git a/build/common.targets b/build/common.targets index c9afb27a..30c059a3 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ - 3.8.4 + 3.9.0 SMAPI latest diff --git a/docs/release-notes.md b/docs/release-notes.md index 1d4323af..0f83e044 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,43 +7,45 @@ * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). --> -## Upcoming release +## 3.9 +Released 22 January 2021 for Stardew Valley 1.5.4 or later. + * For players: * Updated for Stardew Valley 1.5.4. * Improved game detection in the installer: * The installer now prefers paths registered by Steam or GOG Galaxy. * The installer now detects default manual GOG installs. - * Added clearer error when Vortex creates an empty mod folder. - * Fixed various cases where the game's map changes wouldn't be reapplied correctly after mods changed the map. + * Added clearer error text for empty mod folders created by Vortex. + * Fixed the game's map changes not always reapplied correctly after mods change certain maps, which caused issues like the community center resetting to its non-repaired texture. * Fixed compatibility for very old content packs which still load maps from `.xnb` files. These were broken by map loading changes in Stardew Valley 1.5, but SMAPI now corrects them automatically. * Fixed some broken mods incorrectly listed as XNB mods under 'skipped mods'. * For modders: * Added new input APIs: - * Added [API for multi-key bindings](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). + * Added an [API for multi-key bindings](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). * Added a new [`Input.ButtonsChanged` event](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged). * Added a `buttonState.IsDown()` extension. - * Added a `helper.Input.SuppressActiveKeybindings` method which suppresses the active buttons in a keybind list. + * Added a `helper.Input.SuppressActiveKeybinds` method to suppress the active buttons in a keybind list. * Improved multiplayer APIs: * `PerScreen` now lets you get/set the value for any screen, get all active values, or clear all values. - * Peer data for the multiplayer API/events now includes `IsSplitScreen` and `ScreenID` fields. - * Network messages through the multiplayer API are no longer sent to players who don't have SMAPI installed. This reduces unneeded network traffic (since they can't read it anyway) and avoids an error in some cases. + * Peer data from the multiplayer API/events now includes `IsSplitScreen` and `ScreenID` fields. + * Fixed network messages through the multiplayer API being sent to players who don't have SMAPI installed in some cases. * Improved asset propagation: - * Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names). * Updated map propagation for the changes in Stardew Valley 1.5.4. + * Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names). * Fixed quarry bridge not fixed if the mountain map was reloaded. - * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This prevents older mods from loading but bypasses a Visual Studio debugger crash. + * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This prevents older mods from loading, but bypasses a Visual Studio crash when debugging. * Game errors shown in the chatbox are now logged. * Moved vanilla error-handling into a new Error Handler mod. This simplifies the core SMAPI logic, and lets users disable it if needed. * For the Console Commands mod: - * Removed the `inf` option for `player_sethealth`, `player_setmoney`, and `player_setstamina`. You can use more intuitive mods like [CJB Cheats Menu](https://www.nexusmods.com/stardewvalley/mods/4) if you used those options. + * Removed the `inf` option for `player_sethealth`, `player_setmoney`, and `player_setstamina`. You can use mods like [CJB Cheats Menu](https://www.nexusmods.com/stardewvalley/mods/4) instead for that. * For the Error Handler mod: * Added a detailed message for the _Input string was not in a correct format_ error when the game fails to parse an item text description. * For the web UI: - * Fixed JSON validator for manifest files marking some update keys as invalid incorrectly. + * Fixed JSON validator incorrectly marking some manifest update keys as invalid. ## 3.8.4 Released 15 January 2021 for Stardew Valley 1.5.3 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index a7daf62b..f2340638 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.8.4", + "Version": "3.9.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.8.4" + "MinimumApiVersion": "3.9.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 3394da53..bc0a7294 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.8.4", + "Version": "3.9.0", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.8.4" + "MinimumApiVersion": "3.9.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 0fd202da..79727fad 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.8.4", + "Version": "3.9.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.8.4" + "MinimumApiVersion": "3.9.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 36745ea7..2adafbbf 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -54,7 +54,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.4"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.0"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); -- cgit