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 --- src/SMAPI/Framework/SGame.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/SMAPI/Framework/SGame.cs') 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 { -- 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/Framework/SGame.cs') 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