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 + src/SMAPI.sln | 8 + src/SMAPI/Framework/SCore.cs | 31 +--- src/SMAPI/Patches/DialogueErrorPatch.cs | 192 --------------------- src/SMAPI/Patches/EventErrorPatch.cs | 114 ------------ src/SMAPI/Patches/LoadErrorPatch.cs | 157 ----------------- src/SMAPI/Patches/ObjectErrorPatch.cs | 143 --------------- src/SMAPI/Patches/ScheduleErrorPatch.cs | 115 ------------ src/SMAPI/Properties/AssemblyInfo.cs | 1 + src/SMAPI/SMAPI.config.json | 1 + src/SMAPI/i18n/de.json | 4 - src/SMAPI/i18n/default.json | 3 - src/SMAPI/i18n/es.json | 3 - src/SMAPI/i18n/fr.json | 3 - src/SMAPI/i18n/hu.json | 3 - src/SMAPI/i18n/it.json | 3 - src/SMAPI/i18n/ja.json | 3 - src/SMAPI/i18n/ko.json | 3 - src/SMAPI/i18n/pt.json | 3 - src/SMAPI/i18n/ru.json | 3 - src/SMAPI/i18n/tr.json | 3 - src/SMAPI/i18n/zh.json | 3 - 41 files changed, 913 insertions(+), 782 deletions(-) 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 delete mode 100644 src/SMAPI/Patches/DialogueErrorPatch.cs delete mode 100644 src/SMAPI/Patches/EventErrorPatch.cs delete mode 100644 src/SMAPI/Patches/LoadErrorPatch.cs delete mode 100644 src/SMAPI/Patches/ObjectErrorPatch.cs delete mode 100644 src/SMAPI/Patches/ScheduleErrorPatch.cs (limited to 'src') 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" +} diff --git a/src/SMAPI.sln b/src/SMAPI.sln index b7a84fe4..904ae7f4 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\SMAPI.Mods.ConsoleCommands.csproj", "{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ErrorHandler", "SMAPI.Mods.ErrorHandler\SMAPI.Mods.ErrorHandler.csproj", "{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\SMAPI.Mods.SaveBackup.csproj", "{CD53AD6F-97F4-4872-A212-50C2A0FD3601}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit", "SMAPI.Toolkit\SMAPI.Toolkit.csproj", "{08184F74-60AD-4EEE-A78C-F4A35ADE6246}" @@ -83,6 +85,7 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 + SMAPI.Internal\SMAPI.Internal.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5 @@ -121,6 +124,10 @@ Global {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.Build.0 = Debug|Any CPU {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.Build.0 = Release|Any CPU + {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Release|Any CPU.Build.0 = Release|Any CPU {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -151,6 +158,7 @@ Global {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} + {491E775B-EAD0-44D4-B6CA-F1FC3E316D33} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f9a36593..00c2de75 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -124,9 +124,6 @@ namespace StardewModdingAPI.Framework /// 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 - /// Whether custom content was removed from the save data to avoid a crash. - private bool IsSaveContentRemoved; - /// Asset interceptors added or removed since the last tick. private readonly List ReloadAssetInterceptorsQueue = new List(); @@ -145,6 +142,10 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. internal static DeprecationManager DeprecationManager { get; private set; } + /// The singleton instance. + /// This is only intended for use by external code like the Error Handler mod. + internal static SCore Instance { get; private set; } + /// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick. internal static uint TicksElapsed { get; private set; } @@ -157,6 +158,8 @@ namespace StardewModdingAPI.Framework /// Whether to output log messages to the console. public SCore(string modsPath, bool writeToConsole) { + SCore.Instance = this; + // init paths this.VerifyPath(modsPath); this.VerifyPath(Constants.LogDir); @@ -245,12 +248,7 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new EventErrorPatch(this.LogManager.MonitorForGame), - new DialogueErrorPatch(this.LogManager.MonitorForGame, this.Reflection), - new ObjectErrorPatch(), - new LoadContextPatch(this.Reflection, this.OnLoadStageChanged), - new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved), - new ScheduleErrorPatch(this.LogManager.MonitorForGame) + new LoadContextPatch(this.Reflection, this.OnLoadStageChanged) ); // add exit handler @@ -517,15 +515,6 @@ namespace StardewModdingAPI.Framework this.ScreenCommandQueue.GetValueForScreen(screenId).Add(Tuple.Create(command, name, args)); } - /********* - ** Show in-game warnings (for main player only) - *********/ - // save content removed - if (this.IsSaveContentRemoved && Context.IsWorldReady) - { - this.IsSaveContentRemoved = false; - Game1.addHUDMessage(new HUDMessage(this.Translator.Get("warn.invalid-content-removed"), HUDMessage.error_type)); - } /********* ** Run game update @@ -1105,12 +1094,6 @@ namespace StardewModdingAPI.Framework Game1.CustomData[migrationKey] = Constants.ApiVersion.ToString(); } - /// Raised after custom content is removed from the save data to avoid a crash. - internal void OnSaveContentRemoved() - { - this.IsSaveContentRemoved = true; - } - /// A callback invoked before runs. protected void OnNewDayAfterFade() { diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs deleted file mode 100644 index 215df561..00000000 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.Reflection; -using StardewValley; -#if HARMONY_2 -using HarmonyLib; -using StardewModdingAPI.Framework; -#else -using System.Reflection; -using Harmony; -#endif - -namespace StardewModdingAPI.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 Reflector 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, Reflector 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(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/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs deleted file mode 100644 index 46651387..00000000 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ /dev/null @@ -1,114 +0,0 @@ -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.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(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/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs deleted file mode 100644 index f5ee5d71..00000000 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ /dev/null @@ -1,157 +0,0 @@ -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.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/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs deleted file mode 100644 index 64b8e6b6..00000000 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ /dev/null @@ -1,143 +0,0 @@ -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.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(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/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs deleted file mode 100644 index 1d58a292..00000000 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ /dev/null @@ -1,115 +0,0 @@ -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.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(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/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs index ee8a1674..ae758e9b 100644 --- a/src/SMAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("SMAPI.Tests")] +[assembly: InternalsVisibleTo("ErrorHandler")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 6ba64fe7..f44c422f 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -113,6 +113,7 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "SuppressUpdateChecks": [ "SMAPI.ConsoleCommands", + "SMAPI.ErrorHandler", "SMAPI.SaveBackup" ] } diff --git a/src/SMAPI/i18n/de.json b/src/SMAPI/i18n/de.json index a8cbd83b..595c3eff 100644 --- a/src/SMAPI/i18n/de.json +++ b/src/SMAPI/i18n/de.json @@ -1,10 +1,6 @@ { - // error messages - "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}} {{day}}", "generic.date-with-year": "{{season}} {{day}} im Jahr {{year}}" - } diff --git a/src/SMAPI/i18n/default.json b/src/SMAPI/i18n/default.json index 7a3d3ed5..7e1f9c4d 100644 --- a/src/SMAPI/i18n/default.json +++ b/src/SMAPI/i18n/default.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}} {{day}}", diff --git a/src/SMAPI/i18n/es.json b/src/SMAPI/i18n/es.json index c9843991..76228d7d 100644 --- a/src/SMAPI/i18n/es.json +++ b/src/SMAPI/i18n/es.json @@ -1,7 +1,4 @@ { - // error 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).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{seasonLowercase}} {{day}}", diff --git a/src/SMAPI/i18n/fr.json b/src/SMAPI/i18n/fr.json index 5969aa20..e32ee712 100644 --- a/src/SMAPI/i18n/fr.json +++ b/src/SMAPI/i18n/fr.json @@ -1,7 +1,4 @@ { - // error 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).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{day}} {{seasonLowercase}}", diff --git a/src/SMAPI/i18n/hu.json b/src/SMAPI/i18n/hu.json index 785012f4..2e3b7264 100644 --- a/src/SMAPI/i18n/hu.json +++ b/src/SMAPI/i18n/hu.json @@ -1,7 +1,4 @@ { - // error 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).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}} {{day}}", diff --git a/src/SMAPI/i18n/it.json b/src/SMAPI/i18n/it.json index 3b3351c3..7ada11f0 100644 --- a/src/SMAPI/i18n/it.json +++ b/src/SMAPI/i18n/it.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{day}} {{season}}", diff --git a/src/SMAPI/i18n/ja.json b/src/SMAPI/i18n/ja.json index 1f814bfa..c95ac1b1 100644 --- a/src/SMAPI/i18n/ja.json +++ b/src/SMAPI/i18n/ja.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}} {{day}}日", diff --git a/src/SMAPI/i18n/ko.json b/src/SMAPI/i18n/ko.json index d5bbffa4..8d267e5e 100644 --- a/src/SMAPI/i18n/ko.json +++ b/src/SMAPI/i18n/ko.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}} {{day}}", diff --git a/src/SMAPI/i18n/pt.json b/src/SMAPI/i18n/pt.json index e8460922..7a08b08f 100644 --- a/src/SMAPI/i18n/pt.json +++ b/src/SMAPI/i18n/pt.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}} {{day}}", diff --git a/src/SMAPI/i18n/ru.json b/src/SMAPI/i18n/ru.json index 002fdbf8..b8ff55c4 100644 --- a/src/SMAPI/i18n/ru.json +++ b/src/SMAPI/i18n/ru.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}}, {{day}}-е число", diff --git a/src/SMAPI/i18n/tr.json b/src/SMAPI/i18n/tr.json index 2a6e83a1..e97a48ba 100644 --- a/src/SMAPI/i18n/tr.json +++ b/src/SMAPI/i18n/tr.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut).", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{day}} {{season}}", diff --git a/src/SMAPI/i18n/zh.json b/src/SMAPI/i18n/zh.json index cdbe3b74..36d459de 100644 --- a/src/SMAPI/i18n/zh.json +++ b/src/SMAPI/i18n/zh.json @@ -1,7 +1,4 @@ { - // error messages - "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)", - // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{season}}{{day}}日", -- cgit