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 From f945349ed40b770c1f7788f659d4ef3980abe3ff Mon Sep 17 00:00:00 2001 From: David Camp Date: Fri, 15 Jan 2021 18:48:29 -0500 Subject: (feat) Disable Mod rewrites if requested --- src/SMAPI/Framework/Logging/LogManager.cs | 5 ++++- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 9 +++++++-- src/SMAPI/Framework/Models/SConfig.cs | 6 +++++- src/SMAPI/Framework/SCore.cs | 4 ++-- src/SMAPI/Metadata/InstructionMetadata.cs | 25 +++++++++++++----------- src/SMAPI/SMAPI.config.json | 6 ++++++ 6 files changed, 38 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index e504218b..cc573427 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -286,12 +286,15 @@ namespace StardewModdingAPI.Framework.Logging /// Log details for settings that don't match the default. /// Whether to enable full console output for developers. /// Whether to check for newer versions of SMAPI and mods on startup. - public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates) + /// ///Whether to rewrite mods, might need to be false to hook up to the Visual Studio Debugger. + public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates, bool rewriteMods) { if (isDeveloperMode) this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); if (!checkForUpdates) this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + if (!rewriteMods) + this.Monitor.Log($"You configured SMAPI to not rewrite potentially broken mods. This is not reccomended except in certain circumstances such as attaching to the Visual Studio debugger. You can enable mod rewrites by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); this.Monitor.VerboseLog("Verbose logging enabled."); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 9fb5384e..d6a32621 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The objects to dispose as part of this instance. private readonly HashSet Disposables = new HashSet(); + /// Whether mods should be re-writen for compatibility. + private readonly bool RewriteMods; + /********* ** Public methods @@ -45,10 +48,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current game platform. /// Encapsulates monitoring and logging. /// Whether to detect paranoid mode issues. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode) + /// Whether to rewrite potentially broken mods or not. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods) { this.Monitor = monitor; this.ParanoidMode = paranoidMode; + this.RewriteMods = rewriteMods; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); // init resolver @@ -308,7 +313,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // find or rewrite code - IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged).ToArray(); + IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged, this.RewriteMods).ToArray(); RecursiveRewriter rewriter = new RecursiveRewriter( module: module, rewriteType: (type, replaceWith) => diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 3a3f6960..a5b27c17 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -20,7 +20,8 @@ namespace StardewModdingAPI.Framework.Models [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", [nameof(VerboseLogging)] = false, - [nameof(LogNetworkTraffic)] = false + [nameof(LogNetworkTraffic)] = false, + [nameof(RewriteMods)] = true }; /// The default values for , to log changes if different. @@ -64,6 +65,9 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public string[] SuppressUpdateChecks { get; set; } + /// Whether to rewrite mods for compatibility. Should only be set to false to facilitate joining to the Visual Studio Debugger. + public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; + /******** ** Public methods diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 00c2de75..06c88851 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -276,7 +276,7 @@ namespace StardewModdingAPI.Framework // log basic info this.LogManager.HandleMarkerFiles(); - this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates); + this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates, this.Settings.RewriteMods); // set window titles this.SetWindowTitles( @@ -1389,7 +1389,7 @@ namespace StardewModdingAPI.Framework // load mods IList skippedMods = new List(); - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) { // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 2c1e14ce..1816e7f9 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -27,29 +27,32 @@ namespace StardewModdingAPI.Metadata /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. /// Whether to detect paranoid mode issues. /// Whether the assembly was rewritten for crossplatform compatibility. - public IEnumerable GetHandlers(bool paranoidMode, bool platformChanged) + /// Whether to return Rewriters + public IEnumerable GetHandlers(bool paranoidMode, bool platformChanged, bool rewriteMods) { /**** ** rewrite CIL to fix incompatible code ****/ // rewrite for crossplatform compatibility - if (platformChanged) - yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); + if (rewriteMods) + { + if (platformChanged) + yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); - // rewrite for Stardew Valley 1.5 - yield return new FieldReplaceRewriter(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture)); - yield return new FieldReplaceRewriter(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)); - yield return new FieldReplaceRewriter(typeof(MineShaft), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)); + // rewrite for Stardew Valley 1.5 + yield return new FieldReplaceRewriter(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture)); + yield return new FieldReplaceRewriter(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)); + yield return new FieldReplaceRewriter(typeof(MineShaft), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)); - // heuristic rewrites - yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); - yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); + // heuristic rewrites + yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); + yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); #if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); #endif - + } /**** ** detect mod issues ****/ diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index f44c422f..7b9f76d4 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -68,6 +68,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "LogNetworkTraffic": false, + /** + * Whether SMAPI should rewrite mods for compatibility. + * Note: This is best left to true unless you are attempting to hook into the Visual Studio Debugger + */ + "RewriteMods": true, + /** * The colors to use for text written to the SMAPI console. * -- cgit From 666f7ad8f9ad431c3f007d84228207e13d2ddbbc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:29 -0500 Subject: tweak recent changes, update release notes --- docs/release-notes.md | 3 +++ src/SMAPI/Framework/Logging/LogManager.cs | 10 +++++----- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 4 ++-- src/SMAPI/Framework/Models/SConfig.cs | 9 +++++---- src/SMAPI/Metadata/InstructionMetadata.cs | 7 ++++--- src/SMAPI/SMAPI.config.json | 12 ++++++------ 6 files changed, 25 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7347560e..fabe7572 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,9 @@ --> ## Upcoming release +* For modders: + * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. + * 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. diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index cc573427..ff00cff7 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -208,7 +208,7 @@ namespace StardewModdingAPI.Framework.Logging // show update alert if (File.Exists(Constants.UpdateMarker)) { - string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split(new [] { '|' }, 2); + string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split(new[] { '|' }, 2); if (SemanticVersion.TryParse(rawUpdateFound[0], out ISemanticVersion updateFound)) { if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) @@ -286,15 +286,15 @@ namespace StardewModdingAPI.Framework.Logging /// Log details for settings that don't match the default. /// Whether to enable full console output for developers. /// Whether to check for newer versions of SMAPI and mods on startup. - /// ///Whether to rewrite mods, might need to be false to hook up to the Visual Studio Debugger. + /// Whether to rewrite mods for compatibility. public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates, bool rewriteMods) { if (isDeveloperMode) - this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); + this.Monitor.Log("You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI.", LogLevel.Info); if (!checkForUpdates) - this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + this.Monitor.Log("You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI.", LogLevel.Warn); if (!rewriteMods) - this.Monitor.Log($"You configured SMAPI to not rewrite potentially broken mods. This is not reccomended except in certain circumstances such as attaching to the Visual Studio debugger. You can enable mod rewrites by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + this.Monitor.Log("You configured SMAPI to not rewrite broken mods. Many older mods may fail to load. You can undo this by reinstalling SMAPI.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); this.Monitor.VerboseLog("Verbose logging enabled."); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index d6a32621..4fae0f44 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The objects to dispose as part of this instance. private readonly HashSet Disposables = new HashSet(); - /// Whether mods should be re-writen for compatibility. + /// Whether to rewrite mods for compatibility. private readonly bool RewriteMods; @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current game platform. /// Encapsulates monitoring and logging. /// Whether to detect paranoid mode issues. - /// Whether to rewrite potentially broken mods or not. + /// Whether to rewrite mods for compatibility. public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods) { this.Monitor = monitor; diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index a5b27c17..dea08717 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -28,6 +28,7 @@ namespace StardewModdingAPI.Framework.Models private static readonly HashSet DefaultSuppressUpdateChecks = new HashSet(StringComparer.OrdinalIgnoreCase) { "SMAPI.ConsoleCommands", + "SMAPI.ErrorHandler", "SMAPI.SaveBackup" }; @@ -56,6 +57,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } + /// Whether SMAPI should rewrite mods for compatibility. + public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; + /// Whether SMAPI should log network traffic. Best combined with , which includes network metadata. public bool LogNetworkTraffic { get; set; } @@ -65,14 +69,11 @@ namespace StardewModdingAPI.Framework.Models /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public string[] SuppressUpdateChecks { get; set; } - /// Whether to rewrite mods for compatibility. Should only be set to false to facilitate joining to the Visual Studio Debugger. - public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; - /******** ** Public methods ********/ - /// Get the settings which have been customised by the player. + /// Get the settings which have been customized by the player. public IDictionary GetCustomSettings() { IDictionary custom = new Dictionary(); diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 1816e7f9..d1699636 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Metadata /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. /// Whether to detect paranoid mode issues. /// Whether the assembly was rewritten for crossplatform compatibility. - /// Whether to return Rewriters + /// Whether to get handlers which rewrite mods for compatibility. public IEnumerable GetHandlers(bool paranoidMode, bool platformChanged, bool rewriteMods) { /**** @@ -49,10 +49,11 @@ namespace StardewModdingAPI.Metadata yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); #if HARMONY_2 - // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) - yield return new Harmony1AssemblyRewriter(); + // rewrite for SMAPI 3.x (Harmony 1.x => 2.0 update) + yield return new Harmony1AssemblyRewriter(); #endif } + /**** ** detect mod issues ****/ diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 7b9f76d4..7a710f14 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -33,6 +33,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "DeveloperMode": true, + /** + * Whether SMAPI should rewrite mods for compatibility. This may prevent older mods from + * loading, but bypasses a Visual Studio crash when debugging. + */ + "RewriteMods": true, + /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as @@ -68,12 +74,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "LogNetworkTraffic": false, - /** - * Whether SMAPI should rewrite mods for compatibility. - * Note: This is best left to true unless you are attempting to hook into the Visual Studio Debugger - */ - "RewriteMods": true, - /** * The colors to use for text written to the SMAPI console. * -- 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') 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 95ad954fa4b761dd32eefa072622cb1168c4d028 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:30 -0500 Subject: allow get/setting PerScreen values by screen ID --- docs/release-notes.md | 1 + src/SMAPI/Utilities/PerScreen.cs | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5409d9ff..be34e653 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ## Upcoming release * For modders: + * Expanded `PerScreen` API: you can now get/set the value for any screen. * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. * For the Error Handler mod: diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 89d08e87..1498488b 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -49,20 +49,20 @@ namespace StardewModdingAPI.Utilities /// Get the value for a given screen ID, creating it if needed. /// The screen ID to check. - internal T GetValueForScreen(int screenId) + public T GetValueForScreen(int screenId) { - this.RemoveDeadPlayers(); + this.RemoveDeadScreens(); return this.States.TryGetValue(screenId, out T state) ? state : this.States[screenId] = this.CreateNewState(); } - /// Set the value for a given screen ID, creating it if needed. + /// Set the value for a given screen ID. /// The screen ID whose value set. /// The value to set. - internal void SetValueForScreen(int screenId, T value) + public void SetValueForScreen(int screenId, T value) { - this.RemoveDeadPlayers(); + this.RemoveDeadScreens(); this.States[screenId] = value; } @@ -70,18 +70,17 @@ namespace StardewModdingAPI.Utilities /********* ** Private methods *********/ - /// Remove players who are no longer have a split-screen index. - /// Returns whether any players were removed. - private void RemoveDeadPlayers() + /// Remove screens which are no longer active. + private void RemoveDeadScreens() { if (this.LastRemovedScreenId == Context.LastRemovedScreenId) return; - this.LastRemovedScreenId = Context.LastRemovedScreenId; - foreach (int id in this.States.Keys.ToArray()) + + foreach (var pair in this.States.ToArray()) { - if (!Context.HasScreenId(id)) - this.States.Remove(id); + if (!Context.HasScreenId(pair.Key)) + this.States.Remove(pair.Key); } } } -- cgit From a9b99c12069bfabca81a74c83eda7f1325c2522a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:31 -0500 Subject: allow resetting a PerScreen field --- docs/release-notes.md | 2 +- src/SMAPI/Utilities/PerScreen.cs | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index be34e653..deac0bc8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For modders: - * Expanded `PerScreen` API: you can now get/set the value for any screen. + * Expanded `PerScreen` API: you can now get/set the value for any screen, or clear all values. * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. * For the Error Handler mod: diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 1498488b..60406d6b 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -11,10 +11,10 @@ namespace StardewModdingAPI.Utilities /********* ** Fields *********/ - /// Create the initial value for a player. + /// Create the initial value for a screen. private readonly Func CreateNewState; - /// The tracked values for each player. + /// The tracked values for each screen. private readonly IDictionary States = new Dictionary(); /// The last value for which this instance was updated. @@ -24,8 +24,8 @@ namespace StardewModdingAPI.Utilities /********* ** Accessors *********/ - /// The value for the current player. - /// The value is initialized the first time it's requested for that player, unless it's set manually first. + /// The value for the current screen. + /// The value is initialized the first time it's requested for that screen, unless it's set manually first. public T Value { get => this.GetValueForScreen(Context.ScreenId); @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Utilities : this(null) { } /// Construct an instance. - /// Create the initial state for a player screen. + /// Create the initial state for a screen. public PerScreen(Func createNewState) { this.CreateNewState = createNewState ?? (() => default); @@ -66,6 +66,12 @@ namespace StardewModdingAPI.Utilities this.States[screenId] = value; } + /// Remove all active values. + public void ResetAllScreens() + { + this.RemoveScreens(p => true); + } + /********* ** Private methods @@ -77,9 +83,16 @@ namespace StardewModdingAPI.Utilities return; this.LastRemovedScreenId = Context.LastRemovedScreenId; + this.RemoveScreens(id => !Context.HasScreenId(id)); + } + + /// Remove screens matching a condition. + /// Returns whether a screen ID should be removed. + private void RemoveScreens(Func shouldRemove) + { foreach (var pair in this.States.ToArray()) { - if (!Context.HasScreenId(pair.Key)) + if (shouldRemove(pair.Key)) this.States.Remove(pair.Key); } } -- cgit From 812251e7ae532d7a2f10d46ff366bf19e67e88d0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:31 -0500 Subject: allow getting all active values from a PerScreen field --- docs/release-notes.md | 2 +- src/SMAPI/Utilities/PerScreen.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index deac0bc8..c36d80ed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For modders: - * Expanded `PerScreen` API: you can now get/set the value for any screen, or clear all values. + * Expanded `PerScreen` API: you can now get/set the value for any screen, get all active values, or clear all values. * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. * For the Error Handler mod: diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 60406d6b..20b8fbce 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -47,6 +47,13 @@ namespace StardewModdingAPI.Utilities this.CreateNewState = createNewState ?? (() => default); } + /// Get all active values by screen ID. This doesn't initialize the value for a screen ID if it's not created yet. + public IEnumerable> GetActiveValues() + { + this.RemoveDeadScreens(); + return this.States.ToArray(); + } + /// Get the value for a given screen ID, creating it if needed. /// The screen ID to check. public T GetValueForScreen(int screenId) -- cgit From 56ca0f5e81b22eafeaec2c51085a82bda1188121 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:32 -0500 Subject: add split-screen info to multiplayer peer --- docs/release-notes.md | 1 + src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 10 ++++- src/SMAPI/Framework/SGame.cs | 4 ++ src/SMAPI/Framework/SGameRunner.cs | 22 ++++++++++- src/SMAPI/Framework/SMultiplayer.cs | 48 ++++++++++++++++++++--- src/SMAPI/IMultiplayerPeer.cs | 7 ++++ 6 files changed, 84 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index c36d80ed..49ee219a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ## Upcoming release * For modders: * Expanded `PerScreen` API: you can now get/set the value for any screen, get all active values, or clear all values. + * Expanded player info received from multiplayer API/events with new `IsSplitScreen` and `ScreenID` fields. * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. * For the Error Handler mod: diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index 5eda71f6..3923700f 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -24,9 +24,15 @@ namespace StardewModdingAPI.Framework.Networking /// public bool IsHost { get; } + /// + public bool IsSplitScreen => this.ScreenID != null; + /// public bool HasSmapi => this.ApiVersion != null; + /// + public int? ScreenID { get; } + /// public GamePlatform? Platform { get; } @@ -45,12 +51,14 @@ namespace StardewModdingAPI.Framework.Networking *********/ /// Construct an instance. /// The player's unique ID. + /// The player's screen ID, if applicable. /// The metadata to copy. /// A method which sends a message to the peer. /// Whether this is a connection to the host player. - public MultiplayerPeer(long playerID, RemoteContextModel model, Action sendMessage, bool isHost) + public MultiplayerPeer(long playerID, int? screenID, RemoteContextModel model, Action sendMessage, bool isHost) { this.PlayerID = playerID; + this.ScreenID = screenID; this.IsHost = isHost; if (model != null) { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 42a712ee..634680a0 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -81,6 +81,9 @@ namespace StardewModdingAPI.Framework /// Whether the game is creating the save file and SMAPI has already raised . public bool IsBetweenCreateEvents { get; set; } + /// The cached value for this instance's player. + public long? PlayerId { get; private set; } + /// Construct a content manager to read game content files. /// This must be static because the game accesses it before the constructor is called. [NonInstancedStatic] @@ -167,6 +170,7 @@ namespace StardewModdingAPI.Framework try { this.OnUpdating(this, gameTime, () => base.Update(gameTime)); + this.PlayerId = Game1.player?.UniqueMultiplayerID; } finally { diff --git a/src/SMAPI/Framework/SGameRunner.cs b/src/SMAPI/Framework/SGameRunner.cs index ae06f513..45e7369c 100644 --- a/src/SMAPI/Framework/SGameRunner.cs +++ b/src/SMAPI/Framework/SGameRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; @@ -46,6 +47,13 @@ namespace StardewModdingAPI.Framework private readonly Action OnGameExiting; + /********* + ** Public methods + *********/ + /// The singleton instance. + public static SGameRunner Instance => (SGameRunner)GameRunner.instance; + + /********* ** Public methods *********/ @@ -99,15 +107,24 @@ namespace StardewModdingAPI.Framework } /// - public override void RemoveGameInstance(Game1 instance) + public override void RemoveGameInstance(Game1 gameInstance) { - base.RemoveGameInstance(instance); + base.RemoveGameInstance(gameInstance); if (this.gameInstances.Count <= 1) EarlyConstants.LogScreenId = null; this.UpdateForSplitScreenChanges(); } + /// Get the screen ID for a given player ID, if the player is local. + /// The player ID to check. + public int? GetScreenId(long playerId) + { + return this.gameInstances + .FirstOrDefault(p => ((SGame)p).PlayerId == playerId) + ?.instanceId; + } + /********* ** Protected methods @@ -136,6 +153,7 @@ namespace StardewModdingAPI.Framework this.OnGameUpdating(gameTime, () => base.Update(gameTime)); } + /// Update metadata when a split screen is added or removed. private void UpdateForSplitScreenChanges() { HashSet oldScreenIds = new HashSet(Context.ActiveScreenIds); diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 2f89fce9..b2257286 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -196,7 +196,13 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); // store peer - MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false); + MultiplayerPeer newPeer = new MultiplayerPeer( + playerID: message.FarmerID, + screenID: this.GetScreenId(message.FarmerID), + model: model, + sendMessage: sendMessage, + isHost: false + ); if (this.Peers.ContainsKey(message.FarmerID)) { this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info); @@ -238,7 +244,13 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID)) { this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); - MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false); + MultiplayerPeer peer = new MultiplayerPeer( + playerID: message.FarmerID, + screenID: this.GetScreenId(message.FarmerID), + model: null, + sendMessage: sendMessage, + isHost: false + ); this.AddPeer(peer, canBeHost: false); } @@ -280,7 +292,13 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); // store peer - MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null); + MultiplayerPeer peer = new MultiplayerPeer( + playerID: message.FarmerID, + screenID: this.GetScreenId(message.FarmerID), + model: model, + sendMessage: sendMessage, + isHost: model?.IsHost ?? this.HostPeer == null + ); if (peer.IsHost && this.HostPeer != null) { this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); @@ -297,7 +315,14 @@ namespace StardewModdingAPI.Framework if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); - this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false); + var peer = new MultiplayerPeer( + playerID: message.FarmerID, + screenID: this.GetScreenId(message.FarmerID), + model: null, + sendMessage: sendMessage, + isHost: true + ); + this.AddPeer(peer, canBeHost: false); } resume(); break; @@ -309,7 +334,13 @@ namespace StardewModdingAPI.Framework // store peer if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) { - peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null); + peer = new MultiplayerPeer( + playerID: message.FarmerID, + screenID: this.GetScreenId(message.FarmerID), + model: null, + sendMessage: sendMessage, + isHost: this.HostPeer == null + ); this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); this.AddPeer(peer, canBeHost: true); } @@ -505,6 +536,13 @@ namespace StardewModdingAPI.Framework } } + /// Get the screen ID for a given player ID, if the player is local. + /// The player ID to check. + private int? GetScreenId(long playerId) + { + return SGameRunner.Instance.GetScreenId(playerId); + } + /// Get all connected player IDs, including the current player. private IEnumerable GetKnownPlayerIDs() { diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs index 0d4d3261..47084174 100644 --- a/src/SMAPI/IMultiplayerPeer.cs +++ b/src/SMAPI/IMultiplayerPeer.cs @@ -14,9 +14,16 @@ namespace StardewModdingAPI /// Whether this is a connection to the host player. bool IsHost { get; } + /// Whether this a local player on the same computer in split-screen mote. + bool IsSplitScreen { get; } + /// Whether the player has SMAPI installed. bool HasSmapi { get; } + /// The player's screen ID, if applicable. + /// See for details. This is only visible to players in split-screen mode. A remote player won't see this value, even if the other players are in split-screen mode. + int? ScreenID { get; } + /// The player's OS platform, if is true. GamePlatform? Platform { get; } -- cgit From 1b52adb4fb6bcbe95f562e51ce914534ca1e64e8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 18:48:32 -0500 Subject: fix build order for new ErrorHandler project --- src/SMAPI.sln | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 904ae7f4..58228ce9 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -61,6 +61,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Installer", "SMAPI.Installer\SMAPI.Installer.csproj", "{0A9BB24F-15FF-4C26-B1A2-81F7AE316518}" ProjectSection(ProjectDependencies) = postProject {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} + {491E775B-EAD0-44D4-B6CA-F1FC3E316D33} = {491E775B-EAD0-44D4-B6CA-F1FC3E316D33} {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {CD53AD6F-97F4-4872-A212-50C2A0FD3601} {E6DA2198-7686-4F1D-B312-4A4DC70884C0} = {E6DA2198-7686-4F1D-B312-4A4DC70884C0} EndProjectSection -- cgit From a5ba931770cf70b071c00f19dbccb594fb245e06 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Jan 2021 21:18:15 -0500 Subject: improve JSON validator schema for manifest update keys --- docs/release-notes.md | 3 +++ src/SMAPI.Web/wwwroot/schemas/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 49ee219a..06a133e8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,9 @@ * 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. +* For the web UI: + * Fixed JSON validator for manifest files marking some update keys as invalid incorrectly. + ## 3.8.4 Released 15 January 2021 for Stardew Valley 1.5.3 or later. diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index 685b515b..0b265201 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -99,7 +99,7 @@ "type": "array", "items": { "type": "string", - "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$", + "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)$", "@errorMessages": { "pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info." } -- cgit From b58d432a22bc39c3135779664293c7beff7b3bd4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Jan 2021 12:21:33 -0500 Subject: subclass chatbox to log game errors --- docs/release-notes.md | 1 + src/SMAPI/Framework/SChatBox.cs | 49 +++++++++++++++++++++++++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 7 ++++++ 3 files changed, 57 insertions(+) create mode 100644 src/SMAPI/Framework/SChatBox.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 06a133e8..8fa1c330 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For modders: * Expanded `PerScreen` API: you can now get/set the value for any screen, get all active values, or clear all values. * Expanded player info received from multiplayer API/events with new `IsSplitScreen` and `ScreenID` fields. + * Game errors shown in the chatbox are now logged. * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. * For the Error Handler mod: diff --git a/src/SMAPI/Framework/SChatBox.cs b/src/SMAPI/Framework/SChatBox.cs new file mode 100644 index 00000000..e000d1cd --- /dev/null +++ b/src/SMAPI/Framework/SChatBox.cs @@ -0,0 +1,49 @@ +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's implementation of the chatbox which intercepts errors for logging. + internal class SChatBox : ChatBox + { + /********* + ** Fields + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + public SChatBox(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// + protected override void runCommand(string command) + { + this.Monitor.Log($"> chat command: {command}"); + base.runCommand(command); + } + + /// + public override void receiveChatMessage(long sourceFarmer, int chatKind, LocalizedContentManager.LanguageCode language, string message) + { + if (chatKind == ChatBox.errorMessage) + { + // log error + this.Monitor.Log(message, LogLevel.Error); + + // add event details if applicable + if (Game1.CurrentEvent != null && message.StartsWith("Event script error:")) + this.Monitor.Log($"In event #{Game1.CurrentEvent.id} for location {Game1.currentLocation?.NameOrUniqueName}", LogLevel.Error); + } + + base.receiveChatMessage(sourceFarmer, chatKind, language, message); + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 06c88851..1b39065f 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1054,6 +1054,13 @@ namespace StardewModdingAPI.Framework this.OnReturnedToTitle(); } + // override chatbox + if (newStage == LoadStage.Loaded) + { + Game1.onScreenMenus.Remove(Game1.chatBox); + Game1.onScreenMenus.Add(Game1.chatBox = new SChatBox(this.LogManager.MonitorForGame)); + } + // raise events this.EventManager.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); if (newStage == LoadStage.None) -- cgit From 516b2fc010ba9a794297ae74b4c5de321ffd0a70 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Jan 2021 14:57:41 -0500 Subject: don't send multiplayer broadcasts to players without SMAPI --- docs/release-notes.md | 1 + src/SMAPI/Framework/SMultiplayer.cs | 34 ++++++++++++---------------------- 2 files changed, 13 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8fa1c330..d448c726 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * Expanded player info received from multiplayer API/events with new `IsSplitScreen` and `ScreenID` fields. * Game errors shown in the chatbox are now logged. * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. + * 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. * 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. diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index b2257286..8e18cc09 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -392,34 +392,24 @@ namespace StardewModdingAPI.Framework if (string.IsNullOrWhiteSpace(fromModID)) throw new ArgumentNullException(nameof(fromModID)); - // get target players - long curPlayerId = Game1.player.UniqueMultiplayerID; - bool sendToSelf = false; - List sendToPeers = new List(); - if (toPlayerIDs == null) - { - sendToSelf = true; - sendToPeers.AddRange(this.Peers.Values); - } - else + // get valid peers + var sendToPeers = this.Peers.Values.Where(p => p.HasSmapi).ToList(); + bool sendToSelf = true; + + // filter by player ID + if (toPlayerIDs != null) { - foreach (long id in toPlayerIDs.Distinct()) - { - if (id == curPlayerId) - sendToSelf = true; - else if (this.Peers.TryGetValue(id, out MultiplayerPeer peer) && peer.HasSmapi) - sendToPeers.Add(peer); - } + var ids = new HashSet(toPlayerIDs); + sendToPeers.RemoveAll(peer => !ids.Contains(peer.PlayerID)); + sendToSelf = ids.Contains(Game1.player.UniqueMultiplayerID); } // filter by mod ID if (toModIDs != null) { - HashSet sendToMods = new HashSet(toModIDs, StringComparer.OrdinalIgnoreCase); - if (sendToSelf && toModIDs.All(id => this.ModRegistry.Get(id) == null)) - sendToSelf = false; - - sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !sendToMods.Contains(mod.ID))); + var ids = new HashSet(toModIDs, StringComparer.OrdinalIgnoreCase); + sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !ids.Contains(mod.ID))); + sendToSelf = sendToSelf && toModIDs.Any(id => this.ModRegistry.Get(id) != null); } // validate recipients -- cgit From cfe2c3975f8be62581195fbfffc41528f22b2ee3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Jan 2021 15:06:04 -0500 Subject: prefer GOG/Steam registry paths when scanning for game folder --- build/find-game-folder.targets | 15 ++++++++------- docs/release-notes.md | 3 +++ .../Framework/GameScanning/GameScanner.cs | 22 +++++++++++----------- 3 files changed, 22 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/build/find-game-folder.targets b/build/find-game-folder.targets index a4200662..02abb54b 100644 --- a/build/find-game-folder.targets +++ b/build/find-game-folder.targets @@ -20,6 +20,14 @@ + + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32)) + $(_SteamLibraryPath)\steamapps\common\Stardew Valley + C:\Program Files\GalaxyClient\Games\Stardew Valley C:\Program Files\GOG Galaxy\Games\Stardew Valley @@ -29,13 +37,6 @@ C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32)) - $(_SteamLibraryPath)\steamapps\common\Stardew Valley diff --git a/docs/release-notes.md b/docs/release-notes.md index d448c726..663ab667 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,9 @@ --> ## Upcoming release +* For players: + * Improved game path detection. The installer now prefers the path installed through Steam or GOG Galaxy. + * For modders: * Expanded `PerScreen` API: you can now get/set the value for any screen, get all active values, or clear all values. * Expanded player info received from multiplayer API/events with new `IsSplitScreen` and `ScreenID` fields. diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index d4c82180..055e3b6d 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -90,14 +90,6 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning case Platform.Windows: { - // Windows - foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) - { - yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; - yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley"; - yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; - } - // Windows registry #if SMAPI_FOR_WINDOWS IDictionary registryKeys = new Dictionary @@ -113,10 +105,18 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning } // via Steam library path - string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); - if (steampath != null) - yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); + string steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); + if (steamPath != null) + yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); #endif + + // Windows + foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) + { + yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; + yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley"; + yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; + } } break; -- cgit From bc71f994ece3943f02f91cdefbbe126cbef5a541 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Jan 2021 15:11:45 -0500 Subject: improve game path detection --- build/find-game-folder.targets | 3 ++- docs/release-notes.md | 2 +- src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/build/find-game-folder.targets b/build/find-game-folder.targets index 02abb54b..0a766ad4 100644 --- a/build/find-game-folder.targets +++ b/build/find-game-folder.targets @@ -31,12 +31,13 @@ C:\Program Files\GalaxyClient\Games\Stardew Valley C:\Program Files\GOG Galaxy\Games\Stardew Valley + C:\Program Files\GOG Games\Stardew Valley C:\Program Files\Steam\steamapps\common\Stardew Valley C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley + C:\Program Files (x86)\GOG Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - diff --git a/docs/release-notes.md b/docs/release-notes.md index 663ab667..bb379898 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Improved game path detection. The installer now prefers the path installed through Steam or GOG Galaxy. + * Improved game path detection in the installer. The installer now prefers the path registered by Steam or GOG Galaxy, and can also now detect the default install path for manual GOG installs. * For modders: * Expanded `PerScreen` API: you can now get/set the value for any screen, get all active values, or clear all values. diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 055e3b6d..785daba3 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -110,11 +110,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); #endif - // Windows + // default paths foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) { yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley"; + yield return $@"{programFiles}\GOG Games\Stardew Valley"; yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; } } -- cgit From 47df90f67cab0f1903f0214fed4795c8bd181fe0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Jan 2021 18:19:46 -0500 Subject: merge sections in asset propagator --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 92 +++++++++++++++---------------- 1 file changed, 46 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index bd1cc50e..72130e05 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -373,52 +373,6 @@ namespace StardewModdingAPI.Metadata case "loosesprites\\suspensionbridge": // SuspensionBridge constructor return this.ReloadSuspensionBridges(content, key); - /**** - ** Content\TileSheets - ****/ - case "tilesheets\\chairtiles": // Game1.LoadContent - MapSeat.mapChairTexture = content.Load(key); - return true; - - case "tilesheets\\critters": // Critter constructor - return this.ReloadCritterTextures(content, key) > 0; - - case "tilesheets\\crops": // Game1.LoadContent - Game1.cropSpriteSheet = content.Load(key); - return true; - - case "tilesheets\\debris": // Game1.LoadContent - Game1.debrisSpriteSheet = content.Load(key); - return true; - - case "tilesheets\\emotes": // Game1.LoadContent - Game1.emoteSpriteSheet = content.Load(key); - return true; - - case "tilesheets\\furniture": // Game1.LoadContent - Furniture.furnitureTexture = content.Load(key); - return true; - - case "tilesheets\\furniturefront": // Game1.LoadContent - Furniture.furnitureFrontTexture = content.Load(key); - return true; - - case "tilesheets\\projectiles": // Game1.LoadContent - Projectile.projectileSheet = content.Load(key); - return true; - - case "tilesheets\\rain": // Game1.LoadContent - Game1.rainTexture = content.Load(key); - return true; - - case "tilesheets\\tools": // Game1.ResetToolSpriteSheet - Game1.ResetToolSpriteSheet(); - return true; - - case "tilesheets\\weapons": // Game1.LoadContent - Tool.weaponsTexture = content.Load(key); - return true; - /**** ** Content\Maps ****/ @@ -469,14 +423,57 @@ namespace StardewModdingAPI.Metadata Bush.texture = new Lazy(() => content.Load(key)); return true; + case "tilesheets\\chairtiles": // Game1.LoadContent + MapSeat.mapChairTexture = content.Load(key); + return true; + case "tilesheets\\craftables": // Game1.LoadContent Game1.bigCraftableSpriteSheet = content.Load(key); return true; + case "tilesheets\\critters": // Critter constructor + return this.ReloadCritterTextures(content, key) > 0; + + case "tilesheets\\crops": // Game1.LoadContent + Game1.cropSpriteSheet = content.Load(key); + return true; + + case "tilesheets\\debris": // Game1.LoadContent + Game1.debrisSpriteSheet = content.Load(key); + return true; + + case "tilesheets\\emotes": // Game1.LoadContent + Game1.emoteSpriteSheet = content.Load(key); + return true; + case "tilesheets\\fruittrees": // FruitTree FruitTree.texture = content.Load(key); return true; + case "tilesheets\\furniture": // Game1.LoadContent + Furniture.furnitureTexture = content.Load(key); + return true; + + case "tilesheets\\furniturefront": // Game1.LoadContent + Furniture.furnitureFrontTexture = content.Load(key); + return true; + + case "tilesheets\\projectiles": // Game1.LoadContent + Projectile.projectileSheet = content.Load(key); + return true; + + case "tilesheets\\rain": // Game1.LoadContent + Game1.rainTexture = content.Load(key); + return true; + + case "tilesheets\\tools": // Game1.ResetToolSpriteSheet + Game1.ResetToolSpriteSheet(); + return true; + + case "tilesheets\\weapons": // Game1.LoadContent + Tool.weaponsTexture = content.Load(key); + return true; + /**** ** Content\TerrainFeatures ****/ @@ -528,6 +525,9 @@ namespace StardewModdingAPI.Metadata return this.ReloadTreeTextures(content, key, Tree.pineTree); } + /**** + ** Dynamic assets + ****/ // dynamic textures if (this.KeyStartsWith(key, "animals\\cat")) return this.ReloadPetOrHorseSprites(content, key); -- cgit From 9fb6d67417ee3f2db0754b135786e052ae308683 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Jan 2021 18:52:31 -0500 Subject: add asset propagation for Strings\StringsFromCSFiles --- docs/release-notes.md | 11 +++++++---- src/SMAPI/Metadata/CoreAssetPropagator.cs | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index bb379898..9a63de7c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,11 +12,14 @@ * Improved game path detection in the installer. The installer now prefers the path registered by Steam or GOG Galaxy, and can also now detect the default install path for manual GOG installs. * For modders: - * Expanded `PerScreen` API: you can now get/set the value for any screen, get all active values, or clear all values. - * Expanded player info received from multiplayer API/events with new `IsSplitScreen` and `ScreenID` fields. + * 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. + * Improved asset propagation: + * Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names). + * 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. * Game errors shown in the chatbox are now logged. - * Added an option to disable rewriting mods for compatibility (thanks to Bpendragon!). This may prevent older mods from loading, but bypasses a Visual Studio crash when debugging. - * 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. * 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. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 72130e05..24b578ad 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -408,6 +408,12 @@ namespace StardewModdingAPI.Metadata case "minigames\\titlebuttons": // TitleMenu return this.ReloadTitleButtons(content, key); + /**** + ** Content\Strings + ****/ + case "strings\\stringsfromcsfiles": + return this.ReloadStringsFromCsFiles(content); + /**** ** Content\TileSheets ****/ @@ -1028,6 +1034,27 @@ namespace StardewModdingAPI.Metadata return true; } + /// Reload cached translations from the Strings\StringsFromCSFiles asset. + /// The content manager through which to reload the asset. + /// Returns whether any data was reloaded. + /// Derived from the . + private bool ReloadStringsFromCsFiles(LocalizedContentManager content) + { + Game1.samBandName = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2156"); + Game1.elliottBookName = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2157"); + + string[] dayNames = this.Reflection.GetField(typeof(Game1), "_shortDayDisplayName").GetValue(); + dayNames[0] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3042"); + dayNames[1] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3043"); + dayNames[2] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3044"); + dayNames[3] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3045"); + dayNames[4] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3046"); + dayNames[5] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3047"); + dayNames[6] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3048"); + + return true; + } + /**** ** Helpers ****/ -- cgit From 00e545715d89f32ab999a3b1f6ae70edec158591 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Jan 2021 23:19:34 -0500 Subject: reset map overrides when reloading a map (#751) --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9a63de7c..bc50b197 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,6 +18,7 @@ * 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. * Improved asset propagation: * Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names). + * Fixed some town patches not reapplied when the map is reloaded in Stardew Valley 1.5. * 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. * Game errors shown in the chatbox are now logged. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 24b578ad..29c4dc1d 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -785,6 +785,7 @@ namespace StardewModdingAPI.Metadata private void ReloadMap(GameLocation location) { // reset patch caches + this.Reflection.GetField>(location, "_appliedMapOverrides").GetValue().Clear(); switch (location) { case Town _: -- cgit From 5676d94fe655c42e50c27b5eae72b9c96cfc2476 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jan 2021 01:05:15 -0500 Subject: reset some missed map cache fields (#751) --- docs/release-notes.md | 3 ++- src/SMAPI/Metadata/CoreAssetPropagator.cs | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index bc50b197..8e31b79c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,7 +18,8 @@ * 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. * Improved asset propagation: * Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names). - * Fixed some town patches not reapplied when the map is reloaded in Stardew Valley 1.5. + * Fixed some of the game's map changes not reapplied after reloading a map in Stardew Valley 1.5. + * 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. * Game errors shown in the chatbox are now logged. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 29c4dc1d..859a1b7a 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -791,6 +791,7 @@ namespace StardewModdingAPI.Metadata case Town _: this.Reflection.GetField(location, "ccRefurbished").SetValue(false); this.Reflection.GetField(location, "isShowingDestroyedJoja").SetValue(false); + this.Reflection.GetField(location, "isShowingSpecialOrdersBoard").SetValue(false); this.Reflection.GetField(location, "isShowingUpgradedPamHouse").SetValue(false); break; @@ -799,6 +800,10 @@ namespace StardewModdingAPI.Metadata case Forest _: this.Reflection.GetField(location, "hasShownCCUpgrade").SetValue(false); break; + + case Mountain _: + this.Reflection.GetField(location, "bridgeRestored").SetValue(false); + break; } // general updates -- cgit From ff16a6567b6137b1aafed3470406d5f5884a5bdc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jan 2021 21:20:25 -0500 Subject: add multi-key binding API (#744) --- docs/release-notes.md | 3 +- .../Converters/SemanticVersionConverter.cs | 4 +- src/SMAPI.sln.DotSettings | 2 + src/SMAPI/Framework/SCore.cs | 1 + src/SMAPI/Framework/SGame.cs | 13 ++ .../Framework/Serialization/KeybindConverter.cs | 89 ++++++++++++ src/SMAPI/Utilities/Keybind.cs | 131 ++++++++++++++++++ src/SMAPI/Utilities/KeybindList.cs | 152 +++++++++++++++++++++ 8 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI/Framework/Serialization/KeybindConverter.cs create mode 100644 src/SMAPI/Utilities/Keybind.cs create mode 100644 src/SMAPI/Utilities/KeybindList.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8e31b79c..edf4481d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * Improved game path detection in the installer. The installer now prefers the path registered by Steam or GOG Galaxy, and can also now detect the default install path for manual GOG installs. * For modders: + * Added [API for multi-key bindings](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). * 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. @@ -22,9 +23,9 @@ * 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. * 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 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. * For the web UI: diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs index 3604956b..cf69104d 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -18,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters ** Accessors *********/ /// Get whether this converter can read JSON. - public override bool CanRead => true; + public override bool CanRead { get; } = true; /// Get whether this converter can write JSON. - public override bool CanWrite => true; + public override bool CanWrite { get; } = true; /********* diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 76e863cc..29d4ade5 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -39,6 +39,8 @@ True True True + True + True True True True diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1b39065f..0c55164c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -208,6 +208,7 @@ namespace StardewModdingAPI.Framework { JsonConverter[] converters = { new ColorConverter(), + new KeybindConverter(), new PointConverter(), new Vector2Converter(), new RectangleConverter() diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 634680a0..af7fa387 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -124,6 +125,18 @@ namespace StardewModdingAPI.Framework this.OnUpdating = onUpdating; } + /// Get the current input state for a button. + /// The button to check. + /// This is intended for use by and shouldn't be used directly in most cases. + internal static SButtonState GetInputState(SButton button) + { + SInputState input = Game1.input as SInputState; + if (input == null) + throw new InvalidOperationException("SMAPI's input state is not in a ready state yet."); + + return input.GetState(button); + } + /********* ** Protected methods diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs new file mode 100644 index 00000000..1bc146f8 --- /dev/null +++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs @@ -0,0 +1,89 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Framework.Serialization +{ + /// Handles deserialization of and models. + internal class KeybindConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// + public override bool CanRead { get; } = true; + + /// + public override bool CanWrite { get; } = true; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return + typeof(Keybind).IsAssignableFrom(objectType) + || typeof(KeybindList).IsAssignableFrom(objectType); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string path = reader.Path; + + // validate JSON type + if (reader.TokenType != JsonToken.String) + throw new SParseException($"Can't parse {nameof(KeybindList)} from {reader.TokenType} node (path: {reader.Path})."); + + // parse raw value + string str = JToken.Load(reader).Value(); + if (objectType == typeof(Keybind)) + { + return Keybind.TryParse(str, out Keybind parsed, out string[] errors) + ? parsed + : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); + } + + if (objectType == typeof(KeybindList)) + { + return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors) + ? parsed + : throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); + } + + throw new SParseException($"Can't parse unexpected type {objectType} from {reader.TokenType} node (path: {reader.Path})."); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected KeybindList ReadString(string str, string path) + { + return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors) + ? parsed + : throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); + } + } +} diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs new file mode 100644 index 00000000..9d6cd6ee --- /dev/null +++ b/src/SMAPI/Utilities/Keybind.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Utilities +{ + /// A single multi-key binding which can be triggered by the player. + /// NOTE: this is part of , and usually shouldn't be used directly. + public class Keybind + { + /********* + ** Accessors + *********/ + /// The buttons that must be down to activate the keybind. + public SButton[] Buttons { get; } + + /// Whether any keys are bound. + public bool IsBound { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The buttons that must be down to activate the keybind. + public Keybind(params SButton[] buttons) + { + this.Buttons = buttons; + this.IsBound = buttons.Any(p => p != SButton.None); + } + + /// Parse a keybind string, if it's valid. + /// The keybind string. See remarks on for format details. + /// The parsed keybind, if valid. + /// The parse errors, if any. + public static bool TryParse(string input, out Keybind parsed, out string[] errors) + { + // empty input + if (string.IsNullOrWhiteSpace(input)) + { + parsed = new Keybind(SButton.None); + errors = new string[0]; + return true; + } + + // parse buttons + string[] rawButtons = input.Split('+'); + SButton[] buttons = new SButton[rawButtons.Length]; + List rawErrors = new List(); + for (int i = 0; i < buttons.Length; i++) + { + string rawButton = rawButtons[i].Trim(); + if (string.IsNullOrWhiteSpace(rawButton)) + rawErrors.Add("Invalid empty button value"); + else if (!Enum.TryParse(rawButton, ignoreCase: true, out SButton button)) + { + string error = $"Invalid button value '{rawButton}'"; + + switch (rawButton.ToLower()) + { + case "shift": + error += $" (did you mean {SButton.LeftShift}?)"; + break; + + case "ctrl": + case "control": + error += $" (did you mean {SButton.LeftControl}?)"; + break; + + case "alt": + error += $" (did you mean {SButton.LeftAlt}?)"; + break; + } + + rawErrors.Add(error); + } + else + buttons[i] = button; + } + + // build result + if (rawErrors.Any()) + { + parsed = null; + errors = rawErrors.ToArray(); + return false; + } + else + { + parsed = new Keybind(buttons); + errors = new string[0]; + return true; + } + } + + /// Get the keybind state relative to the previous tick. + public SButtonState GetState() + { + SButtonState[] states = this.Buttons.Select(SGame.GetInputState).Distinct().ToArray(); + + // single state + if (states.Length == 1) + return states[0]; + + // if any key has no state, the whole set wasn't enabled last tick + if (states.Contains(SButtonState.None)) + return SButtonState.None; + + // mix of held + pressed => pressed + if (states.All(p => p == SButtonState.Pressed || p == SButtonState.Held)) + return SButtonState.Pressed; + + // mix of held + released => released + if (states.All(p => p == SButtonState.Held || p == SButtonState.Released)) + return SButtonState.Released; + + // not down last tick or now + return SButtonState.None; + } + + /// Get a string representation of the keybind. + /// A keybind is serialized to a string like LeftControl + S, where each key is separated with +. The key order is commutative, so LeftControl + S and S + LeftControl are identical. + public override string ToString() + { + return this.Buttons.Length > 0 + ? string.Join(" + ", this.Buttons) + : SButton.None.ToString(); + } + } +} diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs new file mode 100644 index 00000000..f6933af3 --- /dev/null +++ b/src/SMAPI/Utilities/KeybindList.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialization; + +namespace StardewModdingAPI.Utilities +{ + /// A set of multi-key bindings which can be triggered by the player. + public class KeybindList + { + /********* + ** Accessors + *********/ + /// The individual keybinds. + public Keybind[] Keybinds { get; } + + /// Whether any keys are bound. + public bool IsBound { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying keybinds. + /// See or to parse it from a string representation. You can also use this type directly in your config or JSON data models, and it'll be parsed by SMAPI. + public KeybindList(params Keybind[] keybinds) + { + this.Keybinds = keybinds.Where(p => p.IsBound).ToArray(); + this.IsBound = this.Keybinds.Any(); + } + + /// Parse a keybind list from a string, and throw an exception if it's not valid. + /// The keybind string. See remarks on for format details. + /// The format is invalid. + public static KeybindList Parse(string input) + { + return KeybindList.TryParse(input, out KeybindList parsed, out string[] errors) + ? parsed + : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{input}'.\n{string.Join("\n", errors)}"); + } + + /// Try to parse a keybind list from a string. + /// The keybind string. See remarks on for format details. + /// The parsed keybind list, if valid. + /// The errors that occurred while parsing the input, if any. + public static bool TryParse(string input, out KeybindList parsed, out string[] errors) + { + // empty input + if (string.IsNullOrWhiteSpace(input)) + { + parsed = new KeybindList(); + errors = new string[0]; + return true; + } + + // parse buttons + var rawErrors = new List(); + var keybinds = new List(); + foreach (string rawSet in input.Split(',')) + { + if (string.IsNullOrWhiteSpace(rawSet)) + continue; + + if (!Keybind.TryParse(rawSet, out Keybind keybind, out string[] curErrors)) + rawErrors.AddRange(curErrors); + else + keybinds.Add(keybind); + } + + // build result + if (rawErrors.Any()) + { + parsed = null; + errors = rawErrors.ToArray(); + return false; + } + else + { + parsed = new KeybindList(keybinds.ToArray()); + errors = new string[0]; + return true; + } + } + + /// Get the overall keybind list state relative to the previous tick. + /// States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'. + public SButtonState GetState() + { + bool wasPressed = false; + bool isPressed = false; + + foreach (Keybind keybind in this.Keybinds) + { + switch (keybind.GetState()) + { + case SButtonState.Pressed: + isPressed = true; + break; + + case SButtonState.Held: + wasPressed = true; + isPressed = true; + break; + + case SButtonState.Released: + wasPressed = true; + break; + } + } + + if (wasPressed == isPressed) + { + return wasPressed + ? SButtonState.Held + : SButtonState.None; + } + + return wasPressed + ? SButtonState.Released + : SButtonState.Pressed; + } + + /// Get whether any of the button sets are pressed. + public bool IsDown() + { + SButtonState state = this.GetState(); + return state == SButtonState.Pressed || state == SButtonState.Held; + } + + /// Get whether the input binding was just pressed this tick. + public bool JustPressed() + { + return this.GetState() == SButtonState.Pressed; + } + + /// Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned. + public Keybind GetKeybindCurrentlyDown() + { + return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown()); + } + + /// Get a string representation of the input binding. + /// A keybind list is serialized to a string like LeftControl + S, LeftAlt + S, where each multi-key binding is separated with , and the keys within each keybind are separated with +. The key order is commutative, so LeftControl + S and S + LeftControl are identical. + public override string ToString() + { + return this.Keybinds.Length > 0 + ? string.Join(", ", this.Keybinds.Select(p => p.ToString())) + : SButton.None.ToString(); + } + } +} -- cgit From 7e280a066db92c74e957e2a694c922d4c3eae017 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jan 2021 21:47:05 -0500 Subject: add Input.ButtonsChanged event (#744) --- docs/release-notes.md | 1 + src/SMAPI/Events/ButtonsChangedEventArgs.cs | 67 ++++++++++++++++++++++++++++ src/SMAPI/Events/IInputEvents.cs | 3 ++ src/SMAPI/Framework/Events/EventManager.cs | 4 ++ src/SMAPI/Framework/Events/ModInputEvents.cs | 7 +++ src/SMAPI/Framework/SCore.cs | 31 +++++++------ 6 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 src/SMAPI/Events/ButtonsChangedEventArgs.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index edf4481d..5e0e05b6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * For modders: * Added [API for multi-key bindings](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). + * Added a new [`Input.ButtonsChanged` event](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged). * 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. diff --git a/src/SMAPI/Events/ButtonsChangedEventArgs.cs b/src/SMAPI/Events/ButtonsChangedEventArgs.cs new file mode 100644 index 00000000..dda41692 --- /dev/null +++ b/src/SMAPI/Events/ButtonsChangedEventArgs.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when any buttons were pressed or released. + public class ButtonsChangedEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// The buttons that were pressed, held, or released since the previous tick. + private readonly Lazy> ButtonsByState; + + + /********* + ** Accessors + *********/ + /// The current cursor position. + public ICursorPosition Cursor { get; } + + /// The buttons which were pressed since the previous tick. + public IEnumerable Pressed => this.ButtonsByState.Value[SButtonState.Pressed]; + + /// The buttons which were held since the previous tick. + public IEnumerable Held => this.ButtonsByState.Value[SButtonState.Held]; + + /// The buttons which were released since the previous tick. + public IEnumerable Released => this.ButtonsByState.Value[SButtonState.Released]; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cursor position. + /// The game's current input state. + internal ButtonsChangedEventArgs(ICursorPosition cursor, SInputState inputState) + { + this.Cursor = cursor; + this.ButtonsByState = new Lazy>(() => this.GetButtonsByState(inputState)); + } + + + /********* + ** Private methods + *********/ + /// Get the buttons that were pressed, held, or released since the previous tick. + /// The game's current input state. + private Dictionary GetButtonsByState(SInputState inputState) + { + Dictionary lookup = inputState.ButtonStates + .GroupBy(p => p.Value) + .ToDictionary(p => p.Key, p => p.Select(p => p.Key).ToArray()); + + foreach (var state in new[] { SButtonState.Pressed, SButtonState.Held, SButtonState.Released }) + { + if (!lookup.ContainsKey(state)) + lookup[state] = new SButton[0]; + } + + return lookup; + } + } +} diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs index 5c40a438..081c40c0 100644 --- a/src/SMAPI/Events/IInputEvents.cs +++ b/src/SMAPI/Events/IInputEvents.cs @@ -5,6 +5,9 @@ namespace StardewModdingAPI.Events /// Events raised when the player provides input using a controller, keyboard, or mouse. public interface IInputEvents { + /// Raised after the player presses or releases any buttons on the keyboard, controller, or mouse. + event EventHandler ButtonsChanged; + /// Raised after the player presses a button on the keyboard, controller, or mouse. event EventHandler ButtonPressed; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 665dbfe3..f4abfffe 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -93,6 +93,9 @@ namespace StardewModdingAPI.Framework.Events /**** ** Input ****/ + /// Raised after the player presses or releases any buttons on the keyboard, controller, or mouse. + public readonly ManagedEvent ButtonsChanged; + /// Raised after the player presses a button on the keyboard, controller, or mouse. public readonly ManagedEvent ButtonPressed; @@ -212,6 +215,7 @@ namespace StardewModdingAPI.Framework.Events this.TimeChanged = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged)); this.ReturnedToTitle = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle)); + this.ButtonsChanged = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonsChanged)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true); diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index ab26ab3e..6f423e5d 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ + /// Raised after the player presses or releases any buttons on the keyboard, controller, or mouse. + public event EventHandler ButtonsChanged + { + add => this.EventManager.ButtonsChanged.Add(value, this.Mod); + remove => this.EventManager.ButtonsChanged.Remove(value); + } + /// Raised after the player presses a button on the keyboard, controller, or mouse. public event EventHandler ButtonPressed { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 0c55164c..1ac361cd 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -817,24 +817,29 @@ namespace StardewModdingAPI.Framework } // raise input button events - foreach (var pair in inputState.ButtonStates) + if (inputState.ButtonStates.Count > 0) { - SButton button = pair.Key; - SButtonState status = pair.Value; + events.ButtonsChanged.Raise(new ButtonsChangedEventArgs(cursor, inputState)); - if (status == SButtonState.Pressed) + foreach (var pair in inputState.ButtonStates) { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: button {button} pressed."); + SButton button = pair.Key; + SButtonState status = pair.Value; - events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); - } - else if (status == SButtonState.Released) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: button {button} released."); + if (status == SButtonState.Pressed) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} pressed."); - events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + } + else if (status == SButtonState.Released) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} released."); + + events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + } } } } -- cgit From 7e90b1c60aa12d8c552a56738711500cab783be0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jan 2021 21:47:31 -0500 Subject: add shortcut method to create a keybind list for a single default keybind (#744) --- src/SMAPI/Utilities/KeybindList.cs | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs index f6933af3..4ae66ab7 100644 --- a/src/SMAPI/Utilities/KeybindList.cs +++ b/src/SMAPI/Utilities/KeybindList.cs @@ -83,6 +83,15 @@ namespace StardewModdingAPI.Utilities } } + /// Get a keybind list for a single keybind. + /// The buttons that must be down to activate the keybind. + public static KeybindList ForSingle(params SButton[] buttons) + { + return new KeybindList( + new Keybind(buttons) + ); + } + /// Get the overall keybind list state relative to the previous tick. /// States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'. public SButtonState GetState() -- cgit From f251f0d06c2942b53dfd69bb79bf12b16a227503 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jan 2021 23:14:03 -0500 Subject: make buttonState.IsDown() extension public (#744) --- docs/release-notes.md | 1 + src/SMAPI/SButtonState.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5e0e05b6..5f67641b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * For modders: * Added [API for multi-key bindings](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). * Added a new [`Input.ButtonsChanged` event](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged). + * Added a `buttonState.IsDown()` extension. * 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. diff --git a/src/SMAPI/SButtonState.cs b/src/SMAPI/SButtonState.cs index 2b78da27..5f3e8d3c 100644 --- a/src/SMAPI/SButtonState.cs +++ b/src/SMAPI/SButtonState.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI } /// Extension methods for . - internal static class InputStatusExtensions + public static class InputStatusExtensions { /// Whether the button was pressed or held. /// The button state. -- cgit From e40483aab1f6dbcb89f3a1fd1639fc732fe987fc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Jan 2021 23:50:46 -0500 Subject: add method to suppress active keybindings (#744) --- docs/release-notes.md | 8 +++++--- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 14 ++++++++++++++ src/SMAPI/IInputHelper.cs | 6 ++++++ 3 files changed, 25 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5f67641b..83de68e4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,9 +12,11 @@ * Improved game path detection in the installer. The installer now prefers the path registered by Steam or GOG Galaxy, and can also now detect the default install path for manual GOG installs. * For modders: - * Added [API for multi-key bindings](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). - * Added a new [`Input.ButtonsChanged` event](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged). - * Added a `buttonState.IsDown()` extension. + * Added new input APIs: + * Added [API for multi-key bindings](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). + * Added a new [`Input.ButtonsChanged` event](https://stardewcommunitywiki.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. * 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. diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index e1317544..88caf4c3 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -1,5 +1,6 @@ using System; using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -49,6 +50,19 @@ namespace StardewModdingAPI.Framework.ModHelpers this.CurrentInputState().OverrideButton(button, setDown: false); } + /// + public void SuppressActiveKeybinds(KeybindList keybindList) + { + foreach (Keybind keybind in keybindList.Keybinds) + { + if (!keybind.GetState().IsDown()) + continue; + + foreach (SButton button in keybind.Buttons) + this.Suppress(button); + } + } + /// public SButtonState GetState(SButton button) { diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs index e9768c24..2b907b0d 100644 --- a/src/SMAPI/IInputHelper.cs +++ b/src/SMAPI/IInputHelper.cs @@ -1,3 +1,5 @@ +using StardewModdingAPI.Utilities; + namespace StardewModdingAPI { /// Provides an API for checking and changing input state. @@ -18,6 +20,10 @@ namespace StardewModdingAPI /// The button to suppress. void Suppress(SButton button); + /// Suppress the keybinds which are currently down. + /// The keybind list whose active keybinds to suppress. + void SuppressActiveKeybinds(KeybindList keybindList); + /// Get the state of a button. /// The button to check. SButtonState GetState(SButton button); -- cgit From 587d60495e01b1bdacc31acc7e15a87d7f441839 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jan 2021 01:02:49 -0500 Subject: add unit tests for KeybindList (#744) --- src/SMAPI.Tests/Utilities/KeybindListTests.cs | 152 ++++++++++++++++++++++++++ src/SMAPI/Utilities/Keybind.cs | 10 +- src/SMAPI/Utilities/KeybindList.cs | 2 +- 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI.Tests/Utilities/KeybindListTests.cs (limited to 'src') diff --git a/src/SMAPI.Tests/Utilities/KeybindListTests.cs b/src/SMAPI.Tests/Utilities/KeybindListTests.cs new file mode 100644 index 00000000..0bd6ec17 --- /dev/null +++ b/src/SMAPI.Tests/Utilities/KeybindListTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Utilities; + +namespace SMAPI.Tests.Utilities +{ + /// Unit tests for . + [TestFixture] + internal class KeybindListTests + { + /********* + ** Unit tests + *********/ + /**** + ** TryParse + ****/ + /// Assert the parsed fields when constructed from a simple single-key string. + [TestCaseSource(nameof(KeybindListTests.GetAllButtons))] + public void TryParse_SimpleValue(SButton button) + { + // act + bool success = KeybindList.TryParse($"{button}", out KeybindList parsed, out string[] errors); + + // assert + Assert.IsTrue(success, "Parsing unexpectedly failed."); + Assert.IsNotNull(parsed, "The parsed result should not be null."); + Assert.AreEqual(parsed.ToString(), $"{button}"); + Assert.IsNotNull(errors, message: "The errors should never be null."); + Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors."); + } + + /// Assert the parsed fields when constructed from multi-key values. + [TestCase("", ExpectedResult = "None")] + [TestCase(" ", ExpectedResult = "None")] + [TestCase(null, ExpectedResult = "None")] + [TestCase("A + B", ExpectedResult = "A + B")] + [TestCase("A+B", ExpectedResult = "A + B")] + [TestCase(" A+ B ", ExpectedResult = "A + B")] + [TestCase("a +b", ExpectedResult = "A + B")] + [TestCase("a +b, LEFTcontrol + leftALT + LeftSHifT + delete", ExpectedResult = "A + B, LeftControl + LeftAlt + LeftShift + Delete")] + + [TestCase(",", ExpectedResult = "None")] + [TestCase("A,", ExpectedResult = "A")] + [TestCase(",A", ExpectedResult = "A")] + public string TryParse_MultiValues(string input) + { + // act + bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors); + + // assert + Assert.IsTrue(success, "Parsing unexpectedly failed."); + Assert.IsNotNull(parsed, "The parsed result should not be null."); + Assert.IsNotNull(errors, message: "The errors should never be null."); + Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors."); + return parsed.ToString(); + } + + /// Assert invalid values are rejected. + [TestCase("+", "Invalid empty button value")] + [TestCase("A+", "Invalid empty button value")] + [TestCase("+C", "Invalid empty button value")] + [TestCase("A + B +, C", "Invalid empty button value")] + [TestCase("A, TotallyInvalid", "Invalid button value 'TotallyInvalid'")] + [TestCase("A + TotallyInvalid", "Invalid button value 'TotallyInvalid'")] + public void TryParse_InvalidValues(string input, string expectedError) + { + // act + bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors); + + // assert + Assert.IsFalse(success, "Parsing unexpectedly succeeded."); + Assert.IsNull(parsed, "The parsed result should be null."); + Assert.IsNotNull(errors, message: "The errors should never be null."); + Assert.AreEqual(expectedError, string.Join("; ", errors), "The errors don't match the expected ones."); + } + + + /**** + ** GetState + ****/ + /// Assert that returns the expected result for a given input state. + // single value + [TestCase("A", "A:Held", ExpectedResult = SButtonState.Held)] + [TestCase("A", "A:Pressed", ExpectedResult = SButtonState.Pressed)] + [TestCase("A", "A:Released", ExpectedResult = SButtonState.Released)] + [TestCase("A", "A:None", ExpectedResult = SButtonState.None)] + + // multiple values + [TestCase("A + B + C, D", "A:Released, B:None, C:None, D:Pressed", ExpectedResult = SButtonState.Pressed)] // right pressed => pressed + [TestCase("A + B + C, D", "A:Pressed, B:Held, C:Pressed, D:None", ExpectedResult = SButtonState.Pressed)] // left pressed => pressed + [TestCase("A + B + C, D", "A:Pressed, B:Pressed, C:Released, D:None", ExpectedResult = SButtonState.None)] // one key released but other keys weren't down last tick => none + [TestCase("A + B + C, D", "A:Held, B:Held, C:Released, D:None", ExpectedResult = SButtonState.Released)] // all three keys were down last tick and now one is released => released + + // transitive + [TestCase("A, B", "A: Released, B: Pressed", ExpectedResult = SButtonState.Held)] + public SButtonState GetState(string input, string stateMap) + { + // act + bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors); + if (success && parsed?.Keybinds != null) + { + foreach (var keybind in parsed.Keybinds) +#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests + keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap); +#pragma warning restore 618 + } + + // assert + Assert.IsTrue(success, "Parsing unexpected failed"); + Assert.IsNotNull(parsed, "The parsed result should not be null."); + Assert.IsNotNull(errors, message: "The errors should never be null."); + Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors."); + return parsed.GetState(); + } + + + /********* + ** Private methods + *********/ + /// Get all defined buttons. + private static IEnumerable GetAllButtons() + { + foreach (SButton button in Enum.GetValues(typeof(SButton))) + yield return button; + } + + /// Get the button state defined by a mapping string. + /// The button to check. + /// The state map. + private SButtonState GetStateFromMap(SButton button, string stateMap) + { + foreach (string rawPair in stateMap.Split(',')) + { + // parse values + string[] parts = rawPair.Split(new[] { ':' }, 2); + if (!Enum.TryParse(parts[0], ignoreCase: true, out SButton curButton)) + Assert.Fail($"The state map is invalid: unknown button value '{parts[0].Trim()}'"); + if (!Enum.TryParse(parts[1], ignoreCase: true, out SButtonState state)) + Assert.Fail($"The state map is invalid: unknown state value '{parts[1].Trim()}'"); + + // get state + if (curButton == button) + return state; + } + + Assert.Fail($"The state map doesn't define button value '{button}'."); + return SButtonState.None; + } + } +} diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs index 9d6cd6ee..dd8d2861 100644 --- a/src/SMAPI/Utilities/Keybind.cs +++ b/src/SMAPI/Utilities/Keybind.cs @@ -9,6 +9,14 @@ namespace StardewModdingAPI.Utilities /// NOTE: this is part of , and usually shouldn't be used directly. public class Keybind { + /********* + ** Fields + *********/ + /// Get the current input state for a button. + [Obsolete("This property should only be used for unit tests.")] + internal Func GetButtonState { get; set; } = SGame.GetInputState; + + /********* ** Accessors *********/ @@ -97,7 +105,7 @@ namespace StardewModdingAPI.Utilities /// Get the keybind state relative to the previous tick. public SButtonState GetState() { - SButtonState[] states = this.Buttons.Select(SGame.GetInputState).Distinct().ToArray(); + SButtonState[] states = this.Buttons.Select(this.GetButtonState).Distinct().ToArray(); // single state if (states.Length == 1) diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs index 4ae66ab7..1845285a 100644 --- a/src/SMAPI/Utilities/KeybindList.cs +++ b/src/SMAPI/Utilities/KeybindList.cs @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Utilities if (rawErrors.Any()) { parsed = null; - errors = rawErrors.ToArray(); + errors = rawErrors.Distinct().ToArray(); return false; } else -- cgit From 48f6857892ee4e075422f27a7aa5b78bea6b04e0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jan 2021 01:22:29 -0500 Subject: fix null handling in keybind list parsing (#744) --- .../Framework/Serialization/KeybindConverter.cs | 57 ++++++++++------------ 1 file changed, 25 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs index 1bc146f8..7c5db3ad 100644 --- a/src/SMAPI/Framework/Serialization/KeybindConverter.cs +++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs @@ -40,27 +40,34 @@ namespace StardewModdingAPI.Framework.Serialization { string path = reader.Path; - // validate JSON type - if (reader.TokenType != JsonToken.String) - throw new SParseException($"Can't parse {nameof(KeybindList)} from {reader.TokenType} node (path: {reader.Path})."); - - // parse raw value - string str = JToken.Load(reader).Value(); - if (objectType == typeof(Keybind)) + switch (reader.TokenType) { - return Keybind.TryParse(str, out Keybind parsed, out string[] errors) - ? parsed - : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); - } + case JsonToken.Null: + return objectType == typeof(Keybind) + ? new Keybind() + : new KeybindList(); - if (objectType == typeof(KeybindList)) - { - return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors) - ? parsed - : throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); - } + case JsonToken.String: + { + string str = JToken.Load(reader).Value(); + + if (objectType == typeof(Keybind)) + { + return Keybind.TryParse(str, out Keybind parsed, out string[] errors) + ? parsed + : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); + } + else + { + return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors) + ? parsed + : throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); + } + } - throw new SParseException($"Can't parse unexpected type {objectType} from {reader.TokenType} node (path: {reader.Path})."); + default: + throw new SParseException($"Can't parse {objectType} from unexpected {reader.TokenType} node (path: {reader.Path})."); + } } /// Writes the JSON representation of the object. @@ -71,19 +78,5 @@ namespace StardewModdingAPI.Framework.Serialization { writer.WriteValue(value?.ToString()); } - - - /********* - ** Private methods - *********/ - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected KeybindList ReadString(string str, string path) - { - return KeybindList.TryParse(str, out KeybindList parsed, out string[] errors) - ? parsed - : throw new SParseException($"Can't parse {nameof(KeybindList)} from invalid value '{str}' (path: {path}).\n{string.Join("\n", errors)}"); - } } } -- cgit From 4d95030ee9338bf68a9b50ec4482280d3b441c20 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jan 2021 09:31:18 -0500 Subject: correct links --- docs/release-notes.md | 4 ++-- src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 83de68e4..5688982e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,8 +13,8 @@ * For modders: * Added new input APIs: - * Added [API for multi-key bindings](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Input#KeybindList). - * Added a new [`Input.ButtonsChanged` event](https://stardewcommunitywiki.com/Modding:Modder_Guide/APIs/Events#Input.ButtonsChanged). + * Added [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. * Improved multiplayer APIs: diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 3db3856f..665c019b 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -414,7 +414,7 @@ namespace StardewModdingAPI.Framework.ContentManagers int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id); string reason = loadedIndex != -1 - ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewcommunitywiki.com/Modding:Maps#Tilesheet_order for help." + ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help." : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); -- cgit From 49666ac5bcfc0ffb2b8e2b8f2a274f90b67232d2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jan 2021 22:13:12 -0500 Subject: fix SDV 1.5 compatibility with content packs that still load XNB maps --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5688982e..279e43d2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ## Upcoming release * For players: * Improved game path detection in the installer. The installer now prefers the path registered by Steam or GOG Galaxy, and can also now detect the default install path for manual GOG installs. + * 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. * For modders: * Added new input APIs: diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 753ec188..1456d3c1 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (asset is Map map) { map.assetPath = assetName; - this.FixTilesheetPaths(map, relativeMapPath: assetName); + this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: true); } } break; @@ -168,7 +168,7 @@ namespace StardewModdingAPI.Framework.ContentManagers FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); map.assetPath = assetName; - this.FixTilesheetPaths(map, relativeMapPath: assetName); + this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: false); asset = (T)(object)map; } break; @@ -260,8 +260,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. /// The relative map path within the mod folder. + /// Whether to undo the game's eager tilesheet path prefixing for maps loaded from an .xnb file, which incorrectly prefixes tilesheet paths with the map's local asset key folder. /// A map tilesheet couldn't be resolved. - private void FixTilesheetPaths(Map map, string relativeMapPath) + private void FixTilesheetPaths(Map map, string relativeMapPath, bool fixEagerPathPrefixes) { // get map info relativeMapPath = this.AssertAndNormalizeAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators @@ -270,12 +271,16 @@ namespace StardewModdingAPI.Framework.ContentManagers // fix tilesheets foreach (TileSheet tilesheet in map.TileSheets) { + // get image source tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource); - string imageSource = tilesheet.ImageSource; - string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; + + // reverse incorrect eager tilesheet path prefixing + if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource.StartsWith(relativeMapFolder)) + imageSource = imageSource.Substring(relativeMapFolder.Length + 1); // validate tilesheet path + string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../)."); -- cgit From 342fc80394ac2d1bd67fb1b745b8ddec927fac49 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Jan 2021 23:22:24 -0500 Subject: rewrite C# 9 code not supported in Linux build tools yet --- src/SMAPI.Toolkit/SemanticVersion.cs | 12 ++++++------ src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SMultiplayer.cs | 4 ++-- src/SMAPI/Framework/Serialization/KeybindConverter.cs | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index d58dce0c..2f3e282b 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -293,12 +293,12 @@ namespace StardewModdingAPI.Toolkit return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherPlatformRelease, otherTag).ToString(), StringComparison.OrdinalIgnoreCase); } - return CompareToRaw() switch - { - (< 0) => curOlder, - (> 0) => curNewer, - _ => same - }; + int result = CompareToRaw(); + if (result < 0) + return curOlder; + if (result > 0) + return curNewer; + return same; } /// Assert that the current version is valid. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1ac361cd..cd094ff4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -132,7 +132,7 @@ namespace StardewModdingAPI.Framework private readonly ConcurrentQueue RawCommandQueue = new ConcurrentQueue(); /// A list of commands to execute on each screen. - private readonly PerScreen>> ScreenCommandQueue = new(() => new()); + private readonly PerScreen>> ScreenCommandQueue = new PerScreen>>(() => new List>()); /********* diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 8e18cc09..5956b63f 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -56,10 +56,10 @@ namespace StardewModdingAPI.Framework private readonly bool LogNetworkTraffic; /// The backing field for . - private readonly PerScreen> PeersImpl = new(() => new Dictionary()); + private readonly PerScreen> PeersImpl = new PerScreen>(() => new Dictionary()); /// The backing field for . - private readonly PerScreen HostPeerImpl = new(); + private readonly PerScreen HostPeerImpl = new PerScreen(); /********* diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs index 7c5db3ad..93a274a8 100644 --- a/src/SMAPI/Framework/Serialization/KeybindConverter.cs +++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Serialization { case JsonToken.Null: return objectType == typeof(Keybind) - ? new Keybind() + ? (object)new Keybind() : new KeybindList(); case JsonToken.String: -- 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') 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 7fc7a45102f856e84de36fdc5acc0bcb4d99afbb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Jan 2021 19:01:57 -0500 Subject: fix some broken mods incorrectly listed as XNB mods --- docs/release-notes.md | 5 ++- .../Framework/ModScanning/ModScanner.cs | 37 +++++++++++++++------- 2 files changed, 29 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 279e43d2..7e3e48e5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,8 +9,11 @@ ## Upcoming release * For players: - * Improved game path detection in the installer. The installer now prefers the path registered by Steam or GOG Galaxy, and can also now detect the default install path for manual GOG installs. + * 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. * 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: diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 5eacee9e..b2f09431 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -58,16 +58,18 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ".lnk" }; - /// The extensions for files which an XNB mod may contain. If a mod doesn't have a manifest.json and contains *only* these file extensions, it should be considered an XNB mod. - private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + /// The extensions for packed content files. + private readonly HashSet StrictXnbModExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { - // XNB files ".xgs", ".xnb", ".xsb", - ".xwb", + ".xwb" + }; - // unpacking artifacts + /// The extensions for files which an XNB mod may contain, in addition to . + private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + { ".json", ".yaml" }; @@ -118,7 +120,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder."); // XNB mod - if (files.All(this.IsPotentialXnbFile)) + if (this.IsXnbMod(files)) return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); // SMAPI installer @@ -302,14 +304,25 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return !this.IgnoreFilesystemNames.Any(p => p.IsMatch(entry.Name)); } - /// Get whether a file is potentially part of an XNB mod. - /// The file. - private bool IsPotentialXnbFile(FileInfo entry) + /// Get whether a set of files looks like an XNB mod. + /// The files in the mod. + private bool IsXnbMod(IEnumerable files) { - if (!this.IsRelevant(entry)) - return true; + bool hasXnbFile = false; + + foreach (FileInfo file in files.Where(this.IsRelevant)) + { + if (this.StrictXnbModExtensions.Contains(file.Extension)) + { + hasXnbFile = true; + continue; + } + + if (!this.PotentialXnbModExtensions.Contains(file.Extension)) + return false; + } - return this.PotentialXnbModExtensions.Contains(entry.Extension); // use EndsWith to handle cases like image..png + return hasXnbFile; } /// Strip newlines from a string. -- cgit From 546012da8c6ab5e29f34926cda47756d6d18c326 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Jan 2021 19:04:34 -0500 Subject: add clearer error for empty Vortex folders to reduce confusion --- docs/release-notes.md | 1 + .../Framework/ModScanning/ModParseError.cs | 3 ++ .../Framework/ModScanning/ModScanner.cs | 49 ++++++++++++++++++---- 3 files changed, 45 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7e3e48e5..58bc2826 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * 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 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'. diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs index b10510ff..f1e782b6 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs @@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The folder is empty or contains only ignored files. EmptyFolder, + /// The folder is an empty folder managed by Vortex. + EmptyVortexFolder, + /// The folder is ignored by convention. IgnoredFolder, diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index b2f09431..86a97016 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -74,6 +74,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ".yaml" }; + /// The name of the marker file added by Vortex to indicate it's managing the folder. + private readonly string VortexMarkerFileName = "__folder_managed_by_vortex"; + + /// The name for a mod's configuration JSON file. + private readonly string ConfigFileName = "config.json"; + /********* ** Public methods @@ -113,18 +119,24 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // set appropriate invalid-mod error if (manifestFile == null) { - FileInfo[] files = this.RecursivelyGetRelevantFiles(searchFolder).ToArray(); + FileInfo[] files = this.RecursivelyGetFiles(searchFolder).ToArray(); + FileInfo[] relevantFiles = files.Where(this.IsRelevant).ToArray(); + + // empty Vortex folder + // (this filters relevant files internally so it can check for the normally-ignored Vortex marker file) + if (this.IsEmptyVortexFolder(files)) + return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyVortexFolder, "it's an empty Vortex folder (is the mod disabled in Vortex?)."); // empty folder - if (!files.Any()) + if (!relevantFiles.Any()) return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder."); // XNB mod - if (this.IsXnbMod(files)) + if (this.IsXnbMod(relevantFiles)) return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); // SMAPI installer - if (files.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on Mac.command" || p.Name == "install on Windows.bat")) + if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on Mac.command" || p.Name == "install on Windows.bat")) return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file)."); // not a mod? @@ -272,13 +284,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return subfolders.Any() && !files.Any(); } - /// Recursively get all relevant files in a folder based on the result of . + /// Recursively get all files in a folder. /// The root folder to search. - private IEnumerable RecursivelyGetRelevantFiles(DirectoryInfo folder) + private IEnumerable RecursivelyGetFiles(DirectoryInfo folder) { foreach (FileSystemInfo entry in folder.GetFileSystemInfos()) { - if (!this.IsRelevant(entry)) + if (entry is DirectoryInfo && !this.IsRelevant(entry)) continue; if (entry is FileInfo file) @@ -286,7 +298,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning if (entry is DirectoryInfo subfolder) { - foreach (FileInfo subfolderFile in this.RecursivelyGetRelevantFiles(subfolder)) + foreach (FileInfo subfolderFile in this.RecursivelyGetFiles(subfolder)) yield return subfolderFile; } } @@ -325,6 +337,27 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return hasXnbFile; } + /// Get whether a set of files looks like an XNB mod. + /// The files in the mod. + private bool IsEmptyVortexFolder(IEnumerable files) + { + bool hasVortexMarker = false; + + foreach (FileInfo file in files) + { + if (file.Name == this.VortexMarkerFileName) + { + hasVortexMarker = true; + continue; + } + + if (this.IsRelevant(file) && file.Name != this.ConfigFileName) + return false; + } + + return hasVortexMarker; + } + /// Strip newlines from a string. /// The input to strip. private string StripNewlines(string input) -- cgit From 8fd2a6fd3a3de037055ccd8ec350c92ba2cdda9d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jan 2021 20:17:01 -0500 Subject: update for new map override logic in SDV 1.5.4 Special thanks to the Stardew Valley developers for making the requested changes! --- docs/release-notes.md | 4 +++- src/SMAPI/Constants.cs | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 26 ++------------------------ 3 files changed, 6 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 58bc2826..0d5fdea4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,10 +9,12 @@ ## Upcoming release * 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. * 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'. @@ -28,7 +30,7 @@ * 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. * Improved asset propagation: * Added propagation for some `Strings\StringsFromCSFiles` keys (mainly short day names). - * Fixed some of the game's map changes not reapplied after reloading a map in Stardew Valley 1.5. + * Updated map propagation for the changes in Stardew Valley 1.5.4. * 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. * Game errors shown in the chatbox are now logged. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index b72471ca..36745ea7 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -57,7 +57,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.4"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.3"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 859a1b7a..4b911a83 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -784,32 +784,10 @@ namespace StardewModdingAPI.Metadata /// The location whose map to reload. private void ReloadMap(GameLocation location) { - // reset patch caches - this.Reflection.GetField>(location, "_appliedMapOverrides").GetValue().Clear(); - switch (location) - { - case Town _: - this.Reflection.GetField(location, "ccRefurbished").SetValue(false); - this.Reflection.GetField(location, "isShowingDestroyedJoja").SetValue(false); - this.Reflection.GetField(location, "isShowingSpecialOrdersBoard").SetValue(false); - this.Reflection.GetField(location, "isShowingUpgradedPamHouse").SetValue(false); - break; - - case Beach _: - case BeachNightMarket _: - case Forest _: - this.Reflection.GetField(location, "hasShownCCUpgrade").SetValue(false); - break; - - case Mountain _: - this.Reflection.GetField(location, "bridgeRestored").SetValue(false); - break; - } - - // general updates + // reload map location.reloadMap(); - location.updateSeasonalTileSheets(); location.updateWarps(); + location.MakeMapModifications(force: true); // update interior doors location.interiorDoors.Clear(); -- cgit From cea8e557efef1be5466cbb7102b4e163c34394c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jan 2021 20:24:13 -0500 Subject: remove inf options for player_sethealth/money/stamina --- docs/release-notes.md | 3 ++ .../Framework/Commands/Player/SetHealthCommand.cs | 36 ++++------------------ .../Framework/Commands/Player/SetMoneyCommand.cs | 36 ++++------------------ .../Framework/Commands/Player/SetStaminaCommand.cs | 36 ++++------------------ 4 files changed, 21 insertions(+), 90 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0d5fdea4..1d4323af 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -36,6 +36,9 @@ * 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. + * 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. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 59bda5dd..41a5b2af 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -6,19 +6,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// A command which edits the player's current health. internal class SetHealthCommand : TrainerCommand { - /********* - ** Fields - *********/ - /// Whether to keep the player's health at its maximum. - private bool InfiniteHealth; - - /********* ** Public methods *********/ /// Construct an instance. public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { } + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount.") { } /// Handle the command. /// Writes messages to the console and log file. @@ -29,36 +22,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // no-argument mode if (!args.Any()) { - monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info); + monitor.Log($"You currently have {Game1.player.health} health. Specify a value to change it.", LogLevel.Info); return; } // handle string amountStr = args[0]; - if (amountStr == "inf") + if (int.TryParse(amountStr, out int amount)) { - this.InfiniteHealth = true; - monitor.Log("OK, you now have infinite health.", LogLevel.Info); + Game1.player.health = amount; + monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info); } else - { - this.InfiniteHealth = false; - if (int.TryParse(amountStr, out int amount)) - { - Game1.player.health = amount; - monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info); - } - else - this.LogArgumentNotInt(monitor); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void OnUpdated(IMonitor monitor) - { - if (this.InfiniteHealth && Context.IsWorldReady) - Game1.player.health = Game1.player.maxHealth; + this.LogArgumentNotInt(monitor); } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 6e3d68b6..e015f01a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -6,19 +6,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// A command which edits the player's current money. internal class SetMoneyCommand : TrainerCommand { - /********* - ** Fields - *********/ - /// Whether to keep the player's money at a set value. - private bool InfiniteMoney; - - /********* ** Public methods *********/ /// Construct an instance. public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { } + : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount.") { } /// Handle the command. /// Writes messages to the console and log file. @@ -29,36 +22,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // validate if (!args.Any()) { - monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info); + monitor.Log($"You currently have {Game1.player.Money} gold. Specify a value to change it.", LogLevel.Info); return; } // handle string amountStr = args[0]; - if (amountStr == "inf") + if (int.TryParse(amountStr, out int amount)) { - this.InfiniteMoney = true; - monitor.Log("OK, you now have infinite money.", LogLevel.Info); + Game1.player.Money = amount; + monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info); } else - { - this.InfiniteMoney = false; - if (int.TryParse(amountStr, out int amount)) - { - Game1.player.Money = amount; - monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info); - } - else - this.LogArgumentNotInt(monitor); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void OnUpdated(IMonitor monitor) - { - if (this.InfiniteMoney && Context.IsWorldReady) - Game1.player.Money = 999999; + this.LogArgumentNotInt(monitor); } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index 60a1dcb1..a8adafce 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -6,19 +6,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// A command which edits the player's current stamina. internal class SetStaminaCommand : TrainerCommand { - /********* - ** Fields - *********/ - /// Whether to keep the player's stamina at its maximum. - private bool InfiniteStamina; - - /********* ** Public methods *********/ /// Construct an instance. public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { } + : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount.") { } /// Handle the command. /// Writes messages to the console and log file. @@ -29,36 +22,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // validate if (!args.Any()) { - monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info); + monitor.Log($"You currently have {Game1.player.Stamina} stamina. Specify a value to change it.", LogLevel.Info); return; } // handle string amountStr = args[0]; - if (amountStr == "inf") + if (int.TryParse(amountStr, out int amount)) { - this.InfiniteStamina = true; - monitor.Log("OK, you now have infinite stamina.", LogLevel.Info); + Game1.player.Stamina = amount; + monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info); } else - { - this.InfiniteStamina = false; - if (int.TryParse(amountStr, out int amount)) - { - Game1.player.Stamina = amount; - monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info); - } - else - this.LogArgumentNotInt(monitor); - } - } - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public override void OnUpdated(IMonitor monitor) - { - if (this.InfiniteStamina && Context.IsWorldReady) - Game1.player.stamina = Game1.player.MaxStamina; + this.LogArgumentNotInt(monitor); } } } -- cgit From 4b5dd0f2c9ed530e97a783aedb3de73425d24516 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jan 2021 20:29:08 -0500 Subject: rename TrainerCommand to ConsoleCommand to match the rest of the code --- .../Framework/Commands/ConsoleCommand.cs | 115 +++++++++++++++++++++ .../Framework/Commands/IConsoleCommand.cs | 40 +++++++ .../Framework/Commands/ITrainerCommand.cs | 40 ------- .../Commands/Other/ApplySaveFixCommand.cs | 2 +- .../Framework/Commands/Other/DebugCommand.cs | 2 +- .../Commands/Other/ShowDataFilesCommand.cs | 2 +- .../Commands/Other/ShowGameFilesCommand.cs | 2 +- .../Framework/Commands/Other/TestInputCommand.cs | 2 +- .../Framework/Commands/Player/AddCommand.cs | 2 +- .../Commands/Player/ListItemTypesCommand.cs | 2 +- .../Framework/Commands/Player/ListItemsCommand.cs | 2 +- .../Framework/Commands/Player/SetColorCommand.cs | 2 +- .../Framework/Commands/Player/SetHealthCommand.cs | 2 +- .../Commands/Player/SetImmunityCommand.cs | 2 +- .../Commands/Player/SetMaxHealthCommand.cs | 2 +- .../Commands/Player/SetMaxStaminaCommand.cs | 2 +- .../Framework/Commands/Player/SetMoneyCommand.cs | 2 +- .../Framework/Commands/Player/SetNameCommand.cs | 2 +- .../Framework/Commands/Player/SetStaminaCommand.cs | 2 +- .../Framework/Commands/Player/SetStyleCommand.cs | 2 +- .../Framework/Commands/TrainerCommand.cs | 115 --------------------- .../Framework/Commands/World/ClearCommand.cs | 2 +- .../Commands/World/DownMineLevelCommand.cs | 2 +- .../Framework/Commands/World/FreezeTimeCommand.cs | 2 +- .../Framework/Commands/World/SetDayCommand.cs | 2 +- .../Commands/World/SetMineLevelCommand.cs | 2 +- .../Framework/Commands/World/SetSeasonCommand.cs | 2 +- .../Framework/Commands/World/SetTimeCommand.cs | 2 +- .../Framework/Commands/World/SetYearCommand.cs | 2 +- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 20 ++-- 30 files changed, 190 insertions(+), 190 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs new file mode 100644 index 00000000..01cab92e --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands +{ + /// The base implementation for a console command. + internal abstract class ConsoleCommand : IConsoleCommand + { + /********* + ** Accessors + *********/ + /// The command name the user must type. + public string Name { get; } + + /// The command description. + public string Description { get; } + + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + public bool MayNeedInput { get; } + + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + public bool MayNeedUpdate { get; } + + + /********* + ** Public methods + *********/ + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public abstract void Handle(IMonitor monitor, string command, ArgumentParser args); + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public virtual void OnUpdated(IMonitor monitor) { } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public virtual void OnButtonPressed(IMonitor monitor, SButton button) { } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The command name the user must type. + /// The command description. + /// Whether the command may need to perform logic when the player presses a button. + /// Whether the command may need to perform logic when the game updates. + protected ConsoleCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) + { + this.Name = name; + this.Description = description; + this.MayNeedInput = mayNeedInput; + this.MayNeedUpdate = mayNeedUpdate; + } + + /// Log an error indicating incorrect usage. + /// Writes messages to the console and log file. + /// A sentence explaining the problem. + protected void LogUsageError(IMonitor monitor, string error) + { + monitor.Log($"{error} Type 'help {this.Name}' for usage.", LogLevel.Error); + } + + /// Log an error indicating a value must be an integer. + /// Writes messages to the console and log file. + protected void LogArgumentNotInt(IMonitor monitor) + { + this.LogUsageError(monitor, "The value must be a whole number."); + } + + /// Get an ASCII table to show tabular data in the console. + /// The data type. + /// The data to display. + /// The table header. + /// Returns a set of fields for a data value. + protected string GetTableString(IEnumerable data, string[] header, Func getRow) + { + // get table data + int[] widths = header.Select(p => p.Length).ToArray(); + string[][] rows = data + .Select(item => + { + string[] fields = getRow(item); + if (fields.Length != widths.Length) + throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); + + for (int i = 0; i < fields.Length; i++) + widths[i] = Math.Max(widths[i], fields[i].Length); + + return fields; + }) + .ToArray(); + + // render fields + List lines = new List(rows.Length + 2) + { + header, + header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() + }; + lines.AddRange(rows); + + return string.Join( + Environment.NewLine, + lines.Select(line => string.Join(" | ", + line.Select((field, i) => field.PadLeft(widths[i], ' ')) + )) + ); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs new file mode 100644 index 00000000..9c82bbd3 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs @@ -0,0 +1,40 @@ +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands +{ + /// A console command to register. + internal interface IConsoleCommand + { + /********* + ** Accessors + *********/ + /// The command name the user must type. + string Name { get; } + + /// The command description. + string Description { get; } + + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + bool MayNeedUpdate { get; } + + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + bool MayNeedInput { get; } + + + /********* + ** Public methods + *********/ + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + void Handle(IMonitor monitor, string command, ArgumentParser args); + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + void OnUpdated(IMonitor monitor); + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + void OnButtonPressed(IMonitor monitor, SButton button); + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs deleted file mode 100644 index d4d36e5d..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands -{ - /// A console command to register. - internal interface ITrainerCommand - { - /********* - ** Accessors - *********/ - /// The command name the user must type. - string Name { get; } - - /// The command description. - string Description { get; } - - /// Whether the command may need to perform logic when the game updates. This value shouldn't change. - bool MayNeedUpdate { get; } - - /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. - bool MayNeedInput { get; } - - - /********* - ** Public methods - *********/ - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - void Handle(IMonitor monitor, string command, ArgumentParser args); - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - void OnUpdated(IMonitor monitor); - - /// Perform any logic when input is received. - /// Writes messages to the console and log file. - /// The button that was pressed. - void OnButtonPressed(IMonitor monitor, SButton button); - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs index 8f59342e..957b0e75 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs @@ -5,7 +5,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which runs one of the game's save migrations. - internal class ApplySaveFixCommand : TrainerCommand + internal class ApplySaveFixCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs index e4010111..1955c14e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which sends a debug command to the game. - internal class DebugCommand : TrainerCommand + internal class DebugCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs index 54d27185..27f6ce53 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which shows the data files. - internal class ShowDataFilesCommand : TrainerCommand + internal class ShowDataFilesCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs index 0257892f..71093184 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which shows the game files. - internal class ShowGameFilesCommand : TrainerCommand + internal class ShowGameFilesCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs index 11aa10c3..46583dc3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which logs the keys being pressed for 30 seconds once enabled. - internal class TestInputCommand : TrainerCommand + internal class TestInputCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 6cb2b624..0e8f7517 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -7,7 +7,7 @@ using Object = StardewValley.Object; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which adds an item to the player inventory. - internal class AddCommand : TrainerCommand + internal class AddCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs index a835455e..1f12e5f9 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs @@ -4,7 +4,7 @@ using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which list item types. - internal class ListItemTypesCommand : TrainerCommand + internal class ListItemTypesCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index 4232ce16..67569298 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -6,7 +6,7 @@ using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which list items available to spawn. - internal class ListItemsCommand : TrainerCommand + internal class ListItemsCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index f0815ef6..7b7cbf83 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the color of a player feature. - internal class SetColorCommand : TrainerCommand + internal class SetColorCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 41a5b2af..f27b336f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current health. - internal class SetHealthCommand : TrainerCommand + internal class SetHealthCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs index 9c66c4fe..df90adf2 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current immunity. - internal class SetImmunityCommand : TrainerCommand + internal class SetImmunityCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs index f4ae0694..a5f7f444 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's maximum health. - internal class SetMaxHealthCommand : TrainerCommand + internal class SetMaxHealthCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs index 5bce5ea3..e3c2f011 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's maximum stamina. - internal class SetMaxStaminaCommand : TrainerCommand + internal class SetMaxStaminaCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index e015f01a..787ce920 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current money. - internal class SetMoneyCommand : TrainerCommand + internal class SetMoneyCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs index e8cb0584..4911ad1c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -3,7 +3,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's name. - internal class SetNameCommand : TrainerCommand + internal class SetNameCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index a8adafce..c78378ef 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current stamina. - internal class SetStaminaCommand : TrainerCommand + internal class SetStaminaCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs index 31f4107d..98f6c330 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -3,7 +3,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits a player style. - internal class SetStyleCommand : TrainerCommand + internal class SetStyleCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs deleted file mode 100644 index 98daa906..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands -{ - /// The base implementation for a trainer command. - internal abstract class TrainerCommand : ITrainerCommand - { - /********* - ** Accessors - *********/ - /// The command name the user must type. - public string Name { get; } - - /// The command description. - public string Description { get; } - - /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. - public bool MayNeedInput { get; } - - /// Whether the command may need to perform logic when the game updates. This value shouldn't change. - public bool MayNeedUpdate { get; } - - - /********* - ** Public methods - *********/ - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public abstract void Handle(IMonitor monitor, string command, ArgumentParser args); - - /// Perform any logic needed on update tick. - /// Writes messages to the console and log file. - public virtual void OnUpdated(IMonitor monitor) { } - - /// Perform any logic when input is received. - /// Writes messages to the console and log file. - /// The button that was pressed. - public virtual void OnButtonPressed(IMonitor monitor, SButton button) { } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// The command name the user must type. - /// The command description. - /// Whether the command may need to perform logic when the player presses a button. - /// Whether the command may need to perform logic when the game updates. - protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) - { - this.Name = name; - this.Description = description; - this.MayNeedInput = mayNeedInput; - this.MayNeedUpdate = mayNeedUpdate; - } - - /// Log an error indicating incorrect usage. - /// Writes messages to the console and log file. - /// A sentence explaining the problem. - protected void LogUsageError(IMonitor monitor, string error) - { - monitor.Log($"{error} Type 'help {this.Name}' for usage.", LogLevel.Error); - } - - /// Log an error indicating a value must be an integer. - /// Writes messages to the console and log file. - protected void LogArgumentNotInt(IMonitor monitor) - { - this.LogUsageError(monitor, "The value must be a whole number."); - } - - /// Get an ASCII table to show tabular data in the console. - /// The data type. - /// The data to display. - /// The table header. - /// Returns a set of fields for a data value. - protected string GetTableString(IEnumerable data, string[] header, Func getRow) - { - // get table data - int[] widths = header.Select(p => p.Length).ToArray(); - string[][] rows = data - .Select(item => - { - string[] fields = getRow(item); - if (fields.Length != widths.Length) - throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); - - for (int i = 0; i < fields.Length; i++) - widths[i] = Math.Max(widths[i], fields[i].Length); - - return fields; - }) - .ToArray(); - - // render fields - List lines = new List(rows.Length + 2) - { - header, - header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() - }; - lines.AddRange(rows); - - return string.Join( - Environment.NewLine, - lines.Select(line => string.Join(" | ", - line.Select((field, i) => field.PadLeft(widths[i], ' ')) - )) - ); - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 29052be3..4b0e45a0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -10,7 +10,7 @@ using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which clears in-game objects. - internal class ClearCommand : TrainerCommand + internal class ClearCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs index 2cec0fd3..0aa9c9c3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs @@ -4,7 +4,7 @@ using StardewValley.Locations; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which moves the player to the next mine level. - internal class DownMineLevelCommand : TrainerCommand + internal class DownMineLevelCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 736a93a0..16faa2fe 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which freezes the current time. - internal class FreezeTimeCommand : TrainerCommand + internal class FreezeTimeCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs index 23c266ea..4028b3dc 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -5,7 +5,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current day. - internal class SetDayCommand : TrainerCommand + internal class SetDayCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs index b4f6d5b3..40f4b19f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which moves the player to the given mine level. - internal class SetMineLevelCommand : TrainerCommand + internal class SetMineLevelCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index 676369fe..a4cb35bb 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -5,7 +5,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current season. - internal class SetSeasonCommand : TrainerCommand + internal class SetSeasonCommand : ConsoleCommand { /********* ** Fields diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index 9eae6741..6782e38a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -5,7 +5,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current time. - internal class SetTimeCommand : TrainerCommand + internal class SetTimeCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs index 648830c1..95401962 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -5,7 +5,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current year. - internal class SetYearCommand : TrainerCommand + internal class SetYearCommand : ConsoleCommand { /********* ** Public methods diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 5c4f3bba..91437fd3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -13,13 +13,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands ** Fields *********/ /// The commands to handle. - private ITrainerCommand[] Commands; + private IConsoleCommand[] Commands; /// The commands which may need to handle update ticks. - private ITrainerCommand[] UpdateHandlers; + private IConsoleCommand[] UpdateHandlers; /// The commands which may need to handle input. - private ITrainerCommand[] InputHandlers; + private IConsoleCommand[] InputHandlers; /********* @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands { // register commands this.Commands = this.ScanForCommands().ToArray(); - foreach (ITrainerCommand command in this.Commands) + foreach (IConsoleCommand command in this.Commands) helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); // cache commands @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The event arguments. private void OnButtonPressed(object sender, ButtonPressedEventArgs e) { - foreach (ITrainerCommand command in this.InputHandlers) + foreach (IConsoleCommand command in this.InputHandlers) command.OnButtonPressed(this.Monitor, e.Button); } @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The event arguments. private void OnUpdateTicked(object sender, EventArgs e) { - foreach (ITrainerCommand command in this.UpdateHandlers) + foreach (IConsoleCommand command in this.UpdateHandlers) command.OnUpdated(this.Monitor); } @@ -69,19 +69,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The command to invoke. /// The command name specified by the user. /// The command arguments. - private void HandleCommand(ITrainerCommand command, string commandName, string[] args) + private void HandleCommand(IConsoleCommand command, string commandName, string[] args) { ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor); command.Handle(this.Monitor, commandName, argParser); } /// Find all commands in the assembly. - private IEnumerable ScanForCommands() + private IEnumerable ScanForCommands() { return ( from type in this.GetType().Assembly.GetTypes() - where !type.IsAbstract && typeof(ITrainerCommand).IsAssignableFrom(type) - select (ITrainerCommand)Activator.CreateInstance(type) + where !type.IsAbstract && typeof(IConsoleCommand).IsAssignableFrom(type) + select (IConsoleCommand)Activator.CreateInstance(type) ); } } -- 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') 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