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/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 - 20 files changed, 9 insertions(+), 782 deletions(-) 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/SMAPI') 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/SMAPI') 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/SMAPI') 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 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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 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/SMAPI') 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/SMAPI') 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 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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/SMAPI') 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 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/SMAPI') 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 733750fdc4f5d16069d95880144619c0e31e8a89 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Jan 2021 21:04:48 -0500 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 24 +++++++++++++----------- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 21 insertions(+), 19 deletions(-) (limited to 'src/SMAPI') 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