diff options
Diffstat (limited to 'src/SMAPI')
45 files changed, 848 insertions, 940 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index b72471ca..2adafbbf 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -54,10 +54,10 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.4"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.0"); /// <summary>The minimum supported version of Stardew Valley.</summary> - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.3"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); /// <summary>The maximum supported version of Stardew Valley.</summary> public static ISemanticVersion MaximumGameVersion { get; } = null; 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 +{ + /// <summary>Event arguments when any buttons were pressed or released.</summary> + public class ButtonsChangedEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// <summary>The buttons that were pressed, held, or released since the previous tick.</summary> + private readonly Lazy<Dictionary<SButtonState, SButton[]>> ButtonsByState; + + + /********* + ** Accessors + *********/ + /// <summary>The current cursor position.</summary> + public ICursorPosition Cursor { get; } + + /// <summary>The buttons which were pressed since the previous tick.</summary> + public IEnumerable<SButton> Pressed => this.ButtonsByState.Value[SButtonState.Pressed]; + + /// <summary>The buttons which were held since the previous tick.</summary> + public IEnumerable<SButton> Held => this.ButtonsByState.Value[SButtonState.Held]; + + /// <summary>The buttons which were released since the previous tick.</summary> + public IEnumerable<SButton> Released => this.ButtonsByState.Value[SButtonState.Released]; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="cursor">The cursor position.</param> + /// <param name="inputState">The game's current input state.</param> + internal ButtonsChangedEventArgs(ICursorPosition cursor, SInputState inputState) + { + this.Cursor = cursor; + this.ButtonsByState = new Lazy<Dictionary<SButtonState, SButton[]>>(() => this.GetButtonsByState(inputState)); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the buttons that were pressed, held, or released since the previous tick.</summary> + /// <param name="inputState">The game's current input state.</param> + private Dictionary<SButtonState, SButton[]> GetButtonsByState(SInputState inputState) + { + Dictionary<SButtonState, SButton[]> 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 /// <summary>Events raised when the player provides input using a controller, keyboard, or mouse.</summary> public interface IInputEvents { + /// <summary>Raised after the player presses or releases any buttons on the keyboard, controller, or mouse.</summary> + event EventHandler<ButtonsChangedEventArgs> ButtonsChanged; + /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary> event EventHandler<ButtonPressedEventArgs> ButtonPressed; 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); 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 /// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary> /// <param name="map">The map whose tilesheets to fix.</param> /// <param name="relativeMapPath">The relative map path within the mod folder.</param> + /// <param name="fixEagerPathPrefixes">Whether to undo the game's eager tilesheet path prefixing for maps loaded from an <c>.xnb</c> file, which incorrectly prefixes tilesheet paths with the map's local asset key folder.</param> /// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception> - 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 (../)."); 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 ****/ + /// <summary>Raised after the player presses or releases any buttons on the keyboard, controller, or mouse.</summary> + public readonly ManagedEvent<ButtonsChangedEventArgs> ButtonsChanged; + /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary> public readonly ManagedEvent<ButtonPressedEventArgs> ButtonPressed; @@ -212,6 +215,7 @@ namespace StardewModdingAPI.Framework.Events this.TimeChanged = ManageEventOf<TimeChangedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged)); this.ReturnedToTitle = ManageEventOf<ReturnedToTitleEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle)); + this.ButtonsChanged = ManageEventOf<ButtonsChangedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonsChanged)); this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(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 *********/ + /// <summary>Raised after the player presses or releases any buttons on the keyboard, controller, or mouse.</summary> + public event EventHandler<ButtonsChangedEventArgs> ButtonsChanged + { + add => this.EventManager.ButtonsChanged.Add(value, this.Mod); + remove => this.EventManager.ButtonsChanged.Remove(value); + } + /// <summary>Raised after the player presses a button on the keyboard, controller, or mouse.</summary> public event EventHandler<ButtonPressedEventArgs> ButtonPressed { diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index e504218b..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,12 +286,15 @@ namespace StardewModdingAPI.Framework.Logging /// <summary>Log details for settings that don't match the default.</summary> /// <param name="isDeveloperMode">Whether to enable full console output for developers.</param> /// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param> - public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates) + /// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param> + 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 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/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 { @@ -50,6 +51,19 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// <inheritdoc /> + 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); + } + } + + /// <inheritdoc /> public SButtonState GetState(SButton button) { return this.CurrentInputState().GetState(button); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 9fb5384e..4fae0f44 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The objects to dispose as part of this instance.</summary> private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>(); + /// <summary>Whether to rewrite mods for compatibility.</summary> + private readonly bool RewriteMods; + /********* ** Public methods @@ -45,10 +48,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="targetPlatform">The current game platform.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="paranoidMode">Whether to detect paranoid mode issues.</param> - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode) + /// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param> + 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..dea08717 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -20,13 +20,15 @@ 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 }; /// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary> private static readonly HashSet<string> DefaultSuppressUpdateChecks = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "SMAPI.ConsoleCommands", + "SMAPI.ErrorHandler", "SMAPI.SaveBackup" }; @@ -55,6 +57,9 @@ namespace StardewModdingAPI.Framework.Models /// <summary>Whether SMAPI should log more information about the game context.</summary> public bool VerboseLogging { get; set; } + /// <summary>Whether SMAPI should rewrite mods for compatibility.</summary> + public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; + /// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary> public bool LogNetworkTraffic { get; set; } @@ -68,7 +73,7 @@ namespace StardewModdingAPI.Framework.Models /******** ** Public methods ********/ - /// <summary>Get the settings which have been customised by the player.</summary> + /// <summary>Get the settings which have been customized by the player.</summary> public IDictionary<string, object> GetCustomSettings() { IDictionary<string, object> custom = new Dictionary<string, object>(); 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 @@ -25,9 +25,15 @@ namespace StardewModdingAPI.Framework.Networking public bool IsHost { get; } /// <inheritdoc /> + public bool IsSplitScreen => this.ScreenID != null; + + /// <inheritdoc /> public bool HasSmapi => this.ApiVersion != null; /// <inheritdoc /> + public int? ScreenID { get; } + + /// <inheritdoc /> public GamePlatform? Platform { get; } /// <inheritdoc /> @@ -45,12 +51,14 @@ namespace StardewModdingAPI.Framework.Networking *********/ /// <summary>Construct an instance.</summary> /// <param name="playerID">The player's unique ID.</param> + /// <param name="screenID">The player's screen ID, if applicable.</param> /// <param name="model">The metadata to copy.</param> /// <param name="sendMessage">A method which sends a message to the peer.</param> /// <param name="isHost">Whether this is a connection to the host player.</param> - public MultiplayerPeer(long playerID, RemoteContextModel model, Action<OutgoingMessage> sendMessage, bool isHost) + public MultiplayerPeer(long playerID, int? screenID, RemoteContextModel model, Action<OutgoingMessage> sendMessage, bool isHost) { this.PlayerID = playerID; + this.ScreenID = screenID; this.IsHost = isHost; if (model != null) { 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 +{ + /// <summary>SMAPI's implementation of the chatbox which intercepts errors for logging.</summary> + internal class SChatBox : ChatBox + { + /********* + ** Fields + *********/ + /// <summary>Encapsulates monitoring and logging.</summary> + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + public SChatBox(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// <inheritdoc /> + protected override void runCommand(string command) + { + this.Monitor.Log($"> chat command: {command}"); + base.runCommand(command); + } + + /// <inheritdoc /> + 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 f9a36593..cd094ff4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -124,9 +124,6 @@ namespace StardewModdingAPI.Framework /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary> private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second - /// <summary>Whether custom content was removed from the save data to avoid a crash.</summary> - private bool IsSaveContentRemoved; - /// <summary>Asset interceptors added or removed since the last tick.</summary> private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>(); @@ -135,7 +132,7 @@ namespace StardewModdingAPI.Framework private readonly ConcurrentQueue<string> RawCommandQueue = new ConcurrentQueue<string>(); /// <summary>A list of commands to execute on each screen.</summary> - private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new(() => new()); + private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new PerScreen<List<Tuple<Command, string, string[]>>>(() => new List<Tuple<Command, string, string[]>>()); /********* @@ -145,6 +142,10 @@ namespace StardewModdingAPI.Framework /// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks> internal static DeprecationManager DeprecationManager { get; private set; } + /// <summary>The singleton instance.</summary> + /// <remarks>This is only intended for use by external code like the Error Handler mod.</remarks> + internal static SCore Instance { get; private set; } + /// <summary>The number of update ticks which have already executed. This is similar to <see cref="Game1.ticks"/>, but incremented more consistently for every tick.</summary> internal static uint TicksElapsed { get; private set; } @@ -157,6 +158,8 @@ namespace StardewModdingAPI.Framework /// <param name="writeToConsole">Whether to output log messages to the console.</param> public SCore(string modsPath, bool writeToConsole) { + SCore.Instance = this; + // init paths this.VerifyPath(modsPath); this.VerifyPath(Constants.LogDir); @@ -205,6 +208,7 @@ namespace StardewModdingAPI.Framework { JsonConverter[] converters = { new ColorConverter(), + new KeybindConverter(), new PointConverter(), new Vector2Converter(), new RectangleConverter() |
