diff --git a/README.md b/README.md
index 938d12c..2f298c0 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,9 @@ A monogame library for allowing games to interact with cabinet functions.
- [Input](#input-wrapping)
- [ArcadeButtons enum](#arcadebuttons-enum)
- [Get methods](#get-methods)
+- [Event-Based Input](#input-events)
+ - [CButton](#Combined-button)
+ - [Callbacks](#Callbacks)
- [Save data](#save-data)
---
@@ -52,6 +55,122 @@ Given the player it returns a `Vector2` representing the stick direction.
---
---
+## Input Events
+
+The primary difference for event-based input is that instead of calling `Input.Update()` every frame, you should call `InputManager.Update()`
+
+### Combined Button
+
+Combined buttons are generally better for your health than a thousand `||`s and `&&`s between everything [Citation needed]
+
+`CButton` is a class that represents a combined button, which will be 'pressed' when its conditions are met
+
+A CButton can be created from a single button using `CButton.from`
+
+```csharp
+CButton keyA = CButton.from(Keys.A);
+CButton gpX = CButton.from(Buttons.X);
+CButton devB1 = CButton.from(Input.ArcadeButtons.B1);
+```
+
+CButtons can also be combined arbitrarily or inverted
+
+```csharp
+CButton AorX = keyA | gpX;
+CButton AandB1 = keyA & devB1;
+CButton notX = !gpX;
+```
+
+A CButton can be created from an arbitrary number of inputs
+
+```csharp
+CButton anyDirection = CButton.or(Keys.Up, Keys.Down, Keys.Left, Keys.Right);
+CButton sprint = CButton.and(Keys.Shift, Keys.W);
+```
+
+The inputs to `CButton.or()` and `CButton.and()` can be other CButtons, Keys, or Buttons (GamePad or Devcade).
+
+---
+---
+
+### Callbacks
+
+The InputManager uses C#'s `Action`s, which are functions that have no inputs and return `void`
+
+All Actions should be bound in the `Initialize()` method of your game
+
+You have three options for binding an `Action` to a Key:
+
+#### Global Input Manager
+
+The Global input manager represents a group of keybinds that cannot be turned off in any circumstances. For simple games, everything can be done in the global manager, for more control over which keys are bound to what and when, use multiple instances of the Input Manager.
+
+```csharp
+// This is the 'exit' button, which is either the Esc key or both Menu buttons.
+// This is acutally defined in CButton as CButton.exit, so you won't need to
+// make one yourself
+CButton exit = CButton.or(
+ CButton.from(Keys.Escape),
+ CButton.and(
+ CButton.from(Input.ArcadeButtons.Menu, PlayerIndex.One),
+ CButton.from(Input.ArcadeButtons.Menu, PlayerIndex.Two),
+ ),
+);
+
+InputManager.onPressedGlobal(exit, () => { Exit(); });
+
+// You can also use a method group here, which is just a shorthand for the above
+// InputManager.onPressedGlobal(exit, Exit);
+```
+
+#### Static groups
+
+Static management interacts with the InputManager class statically to define groups of keybindings. These groups can be individually enabled and disabled to allow for finer control over which keybinds are active when.
+
+```csharp
+// Class managing input in a submenu
+
+CButton up = CButton.or(Keys.Up, Keys.W, Input.ArcadeButtons.StickUp);
+CButton down = CButton.or(Keys.Down, Keys.S, Input.ArcadeButtons.StickUp);
+
+// This is not necessary, as binding groups are enabled by default.
+// This is only to demonstrate how to enable and disable groups
+InputManager.setEnabled("submenu", true);
+
+InputManager.onPressed(up, "submenu", () => {
+ selected--;
+})
+InputManager.onPressed(down, "submenu", () => {
+ selected++;
+})
+```
+
+#### Instance groups
+
+This uses instances of the InputManager class to define groups of keybindings. This is, under the hood, entirely identical to static management. The only difference is in code style. Both static and non-static management can be used in the same game, and can in fact both refer to the same binding group interchangeably.
+
+```csharp
+// Class managing input in a submenu
+
+CButton up = CButton.or(Keys.Up, Keys.W, Input.ArcadeButtons.StickUp);
+CButton down = CButton.or(Keys.Down, Keys.S, Input.ArcadeButtons.StickUp);
+
+InputManager submenuManager = InputManager.getInputManager("submenu");
+
+// This is not necessary, as binding groups are enabled by default.
+// This is only to demonstrate how to enable and disable groups
+submenuManager.setEnabled(true);
+
+submenuManager.onPressed(up, () => {
+ selected--;
+});
+
+submenuManager.onPressed(down, () => {
+ selected++;
+});
+```
+
+---
## Save data
In the Devcade.SaveData namespace, the SaveManager singleton class has two methods used to save or load text data to or from the cloud.
@@ -65,4 +184,4 @@ Example: `SaveManager.Instance.SaveText("saves/user/save1.txt", "This is save da
### `LoadText(string path)`
Loads data from a given path, returns the loaded data.
-Example: `SaveManager.Instance.LoadText("saves/user/save1.txt");`
\ No newline at end of file
+Example: `SaveManager.Instance.LoadText("saves/user/save1.txt");`
diff --git a/devcade-library/Input.cs b/devcade-library/Input.cs
index b1ee79d..8f4e5dd 100644
--- a/devcade-library/Input.cs
+++ b/devcade-library/Input.cs
@@ -1,18 +1,16 @@
-using Microsoft.Xna.Framework.Input;
+using System;
+using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework;
-namespace Devcade
-{
- public static class Input
- {
+namespace Devcade {
+ public static class Input {
///
/// Enum of button names available on the cabinet. Maps directly to the
/// equivalent in the Buttons enum. Allows you to use existing controller
/// logic and essentially just rename the buttons but you must explicitly
/// cast it to a Button each time.
///
- public enum ArcadeButtons
- {
+ public enum ArcadeButtons {
A1=Buttons.X,
A2=Buttons.Y,
A3=Buttons.RightShoulder,
@@ -35,6 +33,13 @@ public enum ArcadeButtons
public static GamePadState p2State { get; private set; }
public static GamePadState p2LastState { get; private set; }
#endregion
+
+ private static bool externalUpdate;
+ private static bool internalUpdate;
+
+ private class UpdateManagerException : ApplicationException {
+ public UpdateManagerException(string message) : base(message) { }
+ }
///
/// Checks if a button is currently pressed.
@@ -42,12 +47,14 @@ public enum ArcadeButtons
/// The player whose controls should be checked.
/// The button to check.
/// True when button is pressed, false otherwise.
- public static bool GetButton(int playerNum, ArcadeButtons button)
- {
- if (playerNum == 1 && p1State.IsButtonDown((Buttons)button)) { return true; }
- if (playerNum == 2 && p2State.IsButtonDown((Buttons)button)) { return true; }
-
- return false;
+ public static bool GetButton(int playerNum, ArcadeButtons button) {
+ switch (playerNum) {
+ case 1 when p1State.IsButtonDown((Buttons)button):
+ case 2 when p2State.IsButtonDown((Buttons)button):
+ return true;
+ default:
+ return false;
+ }
}
///
@@ -56,12 +63,14 @@ public static bool GetButton(int playerNum, ArcadeButtons button)
/// The player whose controls should be checked.
/// The button to check.
/// True if the button was pressed last frame, false otherwise.
- private static bool GetLastButton(int playerNum, ArcadeButtons button)
- {
- if (playerNum == 1 && p1LastState.IsButtonDown((Buttons)button)) { return true; }
- if (playerNum == 2 && p2LastState.IsButtonDown((Buttons)button)) { return true; }
-
- return false;
+ private static bool GetLastButton(int playerNum, ArcadeButtons button) {
+ switch (playerNum) {
+ case 1 when p1LastState.IsButtonDown((Buttons)button):
+ case 2 when p2LastState.IsButtonDown((Buttons)button):
+ return true;
+ default:
+ return false;
+ }
}
///
@@ -70,8 +79,7 @@ private static bool GetLastButton(int playerNum, ArcadeButtons button)
/// The player whose controls should be checked.
/// The button to check.
/// True if the button transitioned from up to down in the current frame, false otherwise.
- public static bool GetButtonDown(int playerNum, ArcadeButtons button)
- {
+ public static bool GetButtonDown(int playerNum, ArcadeButtons button) {
return (GetButton(playerNum, button) && !GetLastButton(playerNum, button));
}
@@ -81,8 +89,7 @@ public static bool GetButtonDown(int playerNum, ArcadeButtons button)
/// The player whose controls should be checked.
/// The button to check.
/// True if the button transitioned from down to up in the current frame, false otherwise.
- public static bool GetButtonUp(int playerNum, ArcadeButtons button)
- {
+ public static bool GetButtonUp(int playerNum, ArcadeButtons button) {
return (!GetButton(playerNum, button) && GetLastButton(playerNum, button));
}
@@ -92,8 +99,7 @@ public static bool GetButtonUp(int playerNum, ArcadeButtons button)
/// The player whose controls should be checked.
/// The button to check.
/// True if the button was down last frame and is still down, false otherwise.
- public static bool GetButtonHeld(int playerNum, ArcadeButtons button)
- {
+ public static bool GetButtonHeld(int playerNum, ArcadeButtons button) {
return (GetButton(playerNum, button) && GetLastButton(playerNum, button));
}
@@ -102,34 +108,53 @@ public static bool GetButtonHeld(int playerNum, ArcadeButtons button)
///
/// The player whose controls should be checked.
/// A Vector2 representing the stick direction.
- public static Vector2 GetStick(int playerNum)
- {
- if (playerNum == 1) { return p1State.ThumbSticks.Left; }
- if (playerNum == 2) { return p2State.ThumbSticks.Left; }
-
- return Vector2.Zero;
+ public static Vector2 GetStick(int playerNum) {
+ return playerNum switch {
+ 1 => p1State.ThumbSticks.Left,
+ 2 => p2State.ThumbSticks.Left,
+ _ => Vector2.Zero
+ };
}
///
/// Setup initial input states.
///
- public static void Initialize()
- {
+ public static void Initialize() {
p1State = GamePad.GetState(0);
p2State = GamePad.GetState(1);
p1LastState = GamePad.GetState(0);
p2LastState = GamePad.GetState(1);
}
+
+ internal static void UpdateInternal() {
+ internalUpdate = true;
+ if (externalUpdate) {
+ // This exception does not necessarily need to be thrown, it could just ignore the calls, but
+ // calling both Update() methods will probably lead to behavior that is not intended.
+ throw new UpdateManagerException("Cannot use Input.Update() and InputManager.Update() in the same project");
+ }
+ p1LastState = p1State;
+ p2LastState = p2State;
+ p1State = GamePad.GetState(0);
+ p2State = GamePad.GetState(1);
+ }
///
/// Updates input states.
///
- public static void Update()
- {
+ public static void Update() {
+ externalUpdate = true;
+ if (internalUpdate) {
+ throw new UpdateManagerException("Cannot use Input.Update() and InputManager.Update() in the same project");
+ }
p1LastState = p1State;
p2LastState = p2State;
p1State = GamePad.GetState(0);
p2State = GamePad.GetState(1);
}
+
+ public static (GamePadState player1, GamePadState player2) GetStates() {
+ return (player1: p1State, player2: p2State);
+ }
}
}
diff --git a/devcade-library/events/CButton.cs b/devcade-library/events/CButton.cs
new file mode 100644
index 0000000..e6217a8
--- /dev/null
+++ b/devcade-library/events/CButton.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace Devcade.events {
+ public class CButton {
+
+ public static readonly CButton exit = or(
+ Keys.Escape,
+ and(
+ from(Buttons.Start, PlayerIndex.One),
+ from(Buttons.Start, PlayerIndex.Two)
+ )
+ );
+
+ private enum Operator {
+ And,
+ Or,
+ Xor,
+ Not,
+ None
+ }
+
+ private readonly Operator op;
+ private readonly PlayerIndex playerIndex = PlayerIndex.One;
+ private readonly CButton left;
+ private readonly CButton right;
+ private readonly GButton button;
+
+ private CButton(GButton button, PlayerIndex playerIndex) {
+ this.button = button;
+ this.op = Operator.None;
+ this.left = null;
+ this.right = null;
+ this.playerIndex = playerIndex;
+ }
+
+ private CButton(CButton left, CButton right, Operator op) {
+ this.op = op;
+ this.left = left;
+ this.right = right;
+ }
+
+ ///
+ /// Creates a new Compound Button from the given Key
+ ///
+ /// A Keyboard Key
+ ///
+ public static CButton from(Keys key) {
+ return new CButton((GButton)key, PlayerIndex.Four); // PlayerIndex.Four is a dummy value that will always refer to the keyboard
+ }
+
+ ///
+ /// Creates a new Compound Button from the given Button with a default PlayerIndex of One
+ ///
+ /// A Gamepad Button or Devcade Button
+ ///
+ public static CButton from(Buttons btn) {
+ return new CButton((GButton)btn, PlayerIndex.One);
+ }
+
+ ///
+ /// Creates a new Compound Button from the given Button with the given PlayerIndex
+ ///
+ /// A Gamepad Button or Devcade Button
+ /// PlayerIndex of the button
+ ///
+ public static CButton from(Buttons btn, PlayerIndex playerIndex) {
+ return new CButton((GButton)btn, playerIndex);
+ }
+
+ ///
+ /// Creates a new Compound Button that is active when both of the buttons are active
+ ///
+ ///
+ ///
+ ///
+ public static CButton operator &(CButton left, CButton right) {
+ return new CButton(left, right, Operator.And);
+ }
+
+ ///
+ /// Creates a new Compound Button that is active when either of the buttons are active
+ ///
+ ///
+ ///
+ ///
+ public static CButton operator |(CButton left, CButton right) {
+ return new CButton(left, right, Operator.Or);
+ }
+
+ ///
+ /// Creates a new Compound Button that is active when exactly one of the buttons are active
+ ///
+ ///
+ ///
+ ///
+ public static CButton operator ^(CButton left, CButton right) {
+ return new CButton(left, right, Operator.Xor);
+ }
+
+ ///
+ /// Creates a new Compound Button that is active when the given button is not active
+ ///
+ ///
+ ///
+ public static CButton operator !(CButton button) {
+ return new CButton(button, null, Operator.Not);
+ }
+
+ ///
+ /// Creates a Compound Button which is active when any of the buttons in the array are active
+ ///
+ ///
+ ///
+ public static CButton or(params CButton[] btns) {
+ switch (btns.Length) {
+ case 2:
+ return btns[0] | btns[1];
+ case 1:
+ return btns[0];
+ default: {
+ int mid = btns.Length / 2;
+ return or(btns.Take(mid).ToArray()) | or(btns.Skip(mid).ToArray());
+ }
+ }
+ }
+
+ ///
+ /// Creates a Compound Button which is active when any of the buttons in the array are active.
+ ///
+ /// An array of Compound Buttons, Buttons, or Keys
+ ///
+ /// Thrown if one of the input objects is not a valid button type
+ public static CButton or(params object[] btns) {
+ var cbtns = new CButton[btns.Length];
+ for (int i = 0; i < btns.Length; i++) {
+ cbtns[i] = btns[i] switch {
+ CButton _ => (CButton)btns[i],
+ Buttons _ => from((Buttons)btns[i]),
+ Keys _ => from((Keys)btns[i]),
+ _ => throw new ArgumentException("buttons must only contain CompoundButtons, Buttons, or Keys")
+ };
+ }
+ return or(cbtns);
+ }
+
+ ///
+ /// Creates a Compound Button which is active when all of the buttons in the array are active.
+ ///
+ /// An array of Compound Buttons
+ ///
+ public static CButton and(params CButton[] btns) {
+ switch (btns.Length) {
+ case 2:
+ return btns[0] & btns[1];
+ case 1:
+ return btns[0];
+ default: {
+ int mid = btns.Length / 2;
+ return and(btns.Take(mid).ToArray()) & and(btns.Skip(mid).ToArray());
+ }
+ }
+ }
+
+ ///
+ /// Creates a Compound Button which is active when all of the buttons in the array are active.
+ ///
+ /// An array of Compound Buttons, Buttons, or Keys
+ ///
+ /// Thrown if one of the input objects is not a valid button type
+ public static CButton and(params object[] btns) {
+ var cbtns = new CButton[btns.Length];
+ for (int i = 0; i < btns.Length; i++) {
+ cbtns[i] = btns[i] switch {
+ CButton c => c,
+ Buttons b => from(b),
+ Keys k => from(k),
+ _ => throw new ArgumentException("buttons must only contain CompoundButtons, Buttons, or Keys")
+ };
+ }
+ return and(cbtns);
+ }
+
+ internal bool isDown(DevState state) {
+ return op switch {
+ Operator.And => left.isDown(state) && right.isDown(state),
+ Operator.Or => left.isDown(state) || right.isDown(state),
+ Operator.Xor => left.isDown(state) ^ right.isDown(state),
+ Operator.Not => !left.isDown(state),
+ Operator.None => state.isDown(button, playerIndex),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/devcade-library/events/DevState.cs b/devcade-library/events/DevState.cs
new file mode 100644
index 0000000..cbd03dc
--- /dev/null
+++ b/devcade-library/events/DevState.cs
@@ -0,0 +1,53 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace Devcade.events {
+ internal class DevState {
+ private KeyboardState kbState;
+ private GamePadState gp1;
+ private GamePadState gp2;
+
+ public DevState() {
+ this.kbState = Keyboard.GetState();
+ this.gp1 = GamePad.GetState(PlayerIndex.One);
+ this.gp2 = GamePad.GetState(PlayerIndex.Two);
+ }
+
+ public DevState(KeyboardState kbState, GamePadState gp1, GamePadState gp2) {
+ this.kbState = kbState;
+ this.gp1 = gp1;
+ this.gp2 = gp2;
+ }
+
+ private bool isKeyDown(Keys key) {
+ return kbState.IsKeyDown(key);
+ }
+
+ private bool isButtonDown(Buttons button, PlayerIndex player) {
+ switch (player) {
+ case PlayerIndex.One:
+ return gp1.IsButtonDown(button);
+ case PlayerIndex.Two:
+ return gp2.IsButtonDown(button);
+ case PlayerIndex.Three:
+ case PlayerIndex.Four:
+ default:
+ return false;
+ }
+ }
+
+ public bool isDown(GButton button, PlayerIndex playerIndex) {
+ switch (playerIndex) {
+ case PlayerIndex.One:
+ case PlayerIndex.Two:
+ return isButtonDown((Buttons)button, playerIndex);
+ case PlayerIndex.Four:
+ return isKeyDown((Keys)button);
+ case PlayerIndex.Three:
+ default:
+ throw new ArgumentOutOfRangeException(nameof(playerIndex), playerIndex, null);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/devcade-library/events/GButton.cs b/devcade-library/events/GButton.cs
new file mode 100644
index 0000000..7e98ac0
--- /dev/null
+++ b/devcade-library/events/GButton.cs
@@ -0,0 +1,208 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace Devcade.events {
+ internal enum GButton {
+ #region Keyboard
+ kNone = Keys.None,
+ kBack = Keys.Back,
+ kTab = Keys.Tab,
+ kEnter = Keys.Enter,
+ kPause = Keys.Pause,
+ kCapsLock = Keys.CapsLock,
+ kEscape = Keys.Escape,
+ kSpace = Keys.Space,
+ kPageUp = Keys.PageUp,
+ kPageDown = Keys.PageDown,
+ kEnd = Keys.End,
+ kHome = Keys.Home,
+ kLeft = Keys.Left,
+ kUp = Keys.Up,
+ kRight = Keys.Right,
+ kDown = Keys.Down,
+ kSelect = Keys.Select,
+ kPrint = Keys.Print,
+ kExecute = Keys.Execute,
+ kPrintScreen = Keys.PrintScreen,
+ kInsert = Keys.Insert,
+ kDelete = Keys.Delete,
+ kHelp = Keys.Help,
+ kD0 = Keys.D0,
+ kD1 = Keys.D1,
+ kD2 = Keys.D2,
+ kD3 = Keys.D3,
+ kD4 = Keys.D4,
+ kD5 = Keys.D5,
+ kD6 = Keys.D6,
+ kD7 = Keys.D7,
+ kD8 = Keys.D8,
+ kD9 = Keys.D9,
+ kA = Keys.A,
+ kB = Keys.B,
+ kC = Keys.C,
+ kD = Keys.D,
+ kE = Keys.E,
+ kF = Keys.F,
+ kG = Keys.G,
+ kH = Keys.H,
+ kI = Keys.I,
+ kJ = Keys.J,
+ kK = Keys.K,
+ kL = Keys.L,
+ kM = Keys.M,
+ kN = Keys.N,
+ kO = Keys.O,
+ kP = Keys.P,
+ kQ = Keys.Q,
+ kR = Keys.R,
+ kS = Keys.S,
+ kT = Keys.T,
+ kU = Keys.U,
+ kV = Keys.V,
+ kW = Keys.W,
+ kX = Keys.X,
+ kY = Keys.Y,
+ kZ = Keys.Z,
+ kLeftWindows = Keys.LeftWindows,
+ kRightWindows = Keys.RightWindows,
+ kApps = Keys.Apps,
+ kSleep = Keys.Sleep,
+ kNumPad0 = Keys.NumPad0,
+ kNumPad1 = Keys.NumPad1,
+ kNumPad2 = Keys.NumPad2,
+ kNumPad3 = Keys.NumPad3,
+ kNumPad4 = Keys.NumPad4,
+ kNumPad5 = Keys.NumPad5,
+ kNumPad6 = Keys.NumPad6,
+ kNumPad7 = Keys.NumPad7,
+ kNumPad8 = Keys.NumPad8,
+ kNumPad9 = Keys.NumPad9,
+ kMultiply = Keys.Multiply,
+ kAdd = Keys.Add,
+ kSeparator = Keys.Separator,
+ kSubtract = Keys.Subtract,
+ kDecimal = Keys.Decimal,
+ kDivide = Keys.Divide,
+ kF1 = Keys.F1,
+ kF2 = Keys.F2,
+ kF3 = Keys.F3,
+ kF4 = Keys.F4,
+ kF5 = Keys.F5,
+ kF6 = Keys.F6,
+ kF7 = Keys.F7,
+ kF8 = Keys.F8,
+ kF9 = Keys.F9,
+ kF10 = Keys.F10,
+ kF11 = Keys.F11,
+ kF12 = Keys.F12,
+ kF13 = Keys.F13,
+ kF14 = Keys.F14,
+ kF15 = Keys.F15,
+ kF16 = Keys.F16,
+ kF17 = Keys.F17,
+ kF18 = Keys.F18,
+ kF19 = Keys.F19,
+ kF20 = Keys.F20,
+ kF21 = Keys.F21,
+ kF22 = Keys.F22,
+ kF23 = Keys.F23,
+ kF24 = Keys.F24,
+ kNumLock = Keys.NumLock,
+ kScroll = Keys.Scroll,
+ kLeftShift = Keys.LeftShift,
+ kRightShift = Keys.RightShift,
+ kLeftControl = Keys.LeftControl,
+ kRightControl = Keys.RightControl,
+ kLeftAlt = Keys.LeftAlt,
+ kRightAlt = Keys.RightAlt,
+ kBrowserBack = Keys.BrowserBack,
+ kBrowserForward = Keys.BrowserForward,
+ kBrowserRefresh = Keys.BrowserRefresh,
+ kBrowserStop = Keys.BrowserStop,
+ kBrowserSearch = Keys.BrowserSearch,
+ kBrowserFavorites = Keys.BrowserFavorites,
+ kBrowserHome = Keys.BrowserHome,
+ kVolumeMute = Keys.VolumeMute,
+ kVolumeDown = Keys.VolumeDown,
+ kVolumeUp = Keys.VolumeUp,
+ kMediaNextTrack = Keys.MediaNextTrack,
+ kMediaPreviousTrack = Keys.MediaPreviousTrack,
+ kMediaStop = Keys.MediaStop,
+ kMediaPlayPause = Keys.MediaPlayPause,
+ kLaunchMail = Keys.LaunchMail,
+ kSelectMedia = Keys.SelectMedia,
+ kLaunchApplication1 = Keys.LaunchApplication1,
+ kLaunchApplication2 = Keys.LaunchApplication2,
+ kOemSemicolon = Keys.OemSemicolon,
+ kOemPlus = Keys.OemPlus,
+ kOemComma = Keys.OemComma,
+ kOemMinus = Keys.OemMinus,
+ kOemPeriod = Keys.OemPeriod,
+ kOemQuestion = Keys.OemQuestion,
+ kOemTilde = Keys.OemTilde,
+ kChatPadGreen = Keys.ChatPadGreen,
+ kChatPadOrange = Keys.ChatPadOrange,
+ kOemOpenBrackets = Keys.OemOpenBrackets,
+ kOemPipe = Keys.OemPipe,
+ kOemCloseBrackets = Keys.OemCloseBrackets,
+ kOemQuotes = Keys.OemQuotes,
+ kOem8 = Keys.Oem8,
+ kOemBackslash = Keys.OemBackslash,
+ kProcessKey = Keys.ProcessKey,
+ kOemCopy = Keys.OemCopy,
+ kOemAuto = Keys.OemAuto,
+ kOemEnlW = Keys.OemEnlW,
+ kAttn = Keys.Attn,
+ kCrSel = Keys.Crsel,
+ kExSel = Keys.Exsel,
+ kEraseEof = Keys.EraseEof,
+ kPlay = Keys.Play,
+ kZoom = Keys.Zoom,
+ kPa1 = Keys.Pa1,
+ kOemClear = Keys.OemClear,
+ #endregion
+
+ #region Gamepad
+ gDPadUp = Buttons.DPadUp,
+ gDPadDown = Buttons.DPadDown,
+ gDPadLeft = Buttons.DPadLeft,
+ gDPadRight = Buttons.DPadRight,
+ gStart = Buttons.Start,
+ gBack = Buttons.Back,
+ gLeftStick = Buttons.LeftStick,
+ gRightStick = Buttons.RightStick,
+ gLeftShoulder = Buttons.LeftShoulder,
+ gRightShoulder = Buttons.RightShoulder,
+ gBigButton = Buttons.BigButton,
+ gA = Buttons.A,
+ gB = Buttons.B,
+ gX = Buttons.X,
+ gY = Buttons.Y,
+ gLeftThumstickLeft = Buttons.LeftThumbstickLeft,
+ gLeftTrigger = Buttons.LeftTrigger,
+ gRightTrigger = Buttons.RightTrigger,
+ gLeftThumbstickUp = Buttons.LeftThumbstickUp,
+ gLeftThumbstickDown = Buttons.LeftThumbstickDown,
+ gLeftThumbstickRight = Buttons.LeftThumbstickRight,
+ gRightThumbstickLeft = Buttons.RightThumbstickLeft,
+ gRightThumbstickUp = Buttons.RightThumbstickUp,
+ gRightThumbstickDown = Buttons.RightThumbstickDown,
+ gRightThumbstickRight = Buttons.RightThumbstickRight,
+ #endregion
+
+ #region Devcade
+ dA1 = Input.ArcadeButtons.A1,
+ dA2 = Input.ArcadeButtons.A2,
+ dA3 = Input.ArcadeButtons.A3,
+ dA4 = Input.ArcadeButtons.A4,
+ dB1 = Input.ArcadeButtons.B1,
+ dB2 = Input.ArcadeButtons.B2,
+ dB3 = Input.ArcadeButtons.B3,
+ dB4 = Input.ArcadeButtons.B4,
+ dMenu = Input.ArcadeButtons.Menu,
+ dStickUp = Input.ArcadeButtons.StickUp,
+ dStickDown = Input.ArcadeButtons.StickDown,
+ dStickLeft = Input.ArcadeButtons.StickLeft,
+ dStickRight = Input.ArcadeButtons.StickRight,
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/devcade-library/events/InputManager.cs b/devcade-library/events/InputManager.cs
new file mode 100644
index 0000000..dca1f07
--- /dev/null
+++ b/devcade-library/events/InputManager.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace Devcade.events
+{
+ public class InputManager
+ {
+ private static readonly Dictionary inputManagers = new();
+ private static readonly Dictionary enabled = new();
+ private static readonly InputManager globalInputManager = new();
+
+ private const int debounce = 6;
+ private readonly DevState[] _states = new DevState[debounce];
+ private int ptr;
+ private readonly List events = new();
+ public string name { get; }
+
+ private enum State
+ {
+ Held,
+ Released,
+ Pressed,
+ }
+ private struct Event
+ {
+ public CButton button { get; set; }
+ public State state { get; set; }
+ public Action action { get; set; }
+ public bool async { get; set; }
+
+ public bool Invoke() {
+ if (async) {
+ Task.Run(action);
+ }
+ else {
+ action();
+ }
+ return true;
+ }
+
+ public bool Matches(InputManager inputManager) {
+ if (inputManager.name != null /* null is global manager */ && !enabled[inputManager.name]) {
+ return false;
+ }
+ switch (state) {
+ case State.Held:
+ if (inputManager.IsHeld(button)) {
+ return true;
+ }
+ break;
+ case State.Pressed:
+ if (inputManager.IsPressed(button)) {
+ return true;
+ }
+ break;
+ case State.Released:
+ if (inputManager.IsReleased(button)) {
+ return true;
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ return false;
+ }
+ }
+
+ private InputManager(string name) {
+ this.name = name;
+ for(int i = 0; i < debounce; i++) {
+ _states[i] = new DevState();
+ }
+ }
+
+ private InputManager() {
+ this.name = null;
+ for (int i = 0; i < debounce; i++) {
+ _states[i] = new DevState();
+ }
+ }
+
+ ///
+ /// Update Input and invoke events
+ ///
+ public static void Update() {
+ Input.UpdateInternal();
+ (GamePadState, GamePadState) gamePad = Input.GetStates();
+ KeyboardState kbState = Keyboard.GetState();
+ DevState state = new(kbState, gamePad.Item1, gamePad.Item2);
+ globalInputManager.Update(state);
+ foreach (InputManager inputManager in inputManagers.Values) {
+ inputManager.Update(state);
+ }
+ }
+
+ private void Update(DevState state) {
+ ptr++;
+ if (ptr >= debounce) {
+ ptr = 0;
+ }
+ _states[ptr] = state;
+ events.ForEach(e => {
+ bool _ = e.Matches(this) && e.Invoke();
+ });
+ }
+
+ private DevState GetState(int offset) {
+ int index = ptr - offset;
+ if (index < 0) {
+ index += debounce;
+ }
+
+ return _states[index];
+ }
+
+ private bool IsPressed(CButton btn) {
+ return btn.isDown(GetState(0)) && !btn.isDown(GetState(1));
+ }
+
+ private bool IsReleased(CButton btn) {
+ return !btn.isDown(GetState(0)) && btn.isDown(GetState(1));
+ }
+
+ private bool IsHeld(CButton btn) {
+ int acc = 0;
+ for (int i = 0; i < debounce; i++) {
+ if (btn.isDown(GetState(i))) {
+ acc++;
+ }
+ else {
+ return false; // if any of the previous frames were not held, then it is not held
+ }
+ if (acc > 1) return true;
+ }
+
+ return false;
+ }
+
+ public void OnPressed(CButton btn, Action action, bool async = false) {
+ events.Add(new Event {
+ button = btn,
+ state = State.Pressed,
+ action = action,
+ async = async
+ });
+ }
+
+ public void OnReleased(CButton btn, Action action, bool async = false) {
+ events.Add(new Event {
+ button = btn,
+ state = State.Released,
+ action = action,
+ async = async
+ });
+ }
+
+ public void OnHeld(CButton btn, Action action, bool async = false) {
+ events.Add(new Event {
+ button = btn,
+ state = State.Held,
+ action = action,
+ async = async
+ });
+ }
+
+ public static void OnPressedGlobal(CButton btn, Action action, bool async = false) {
+ globalInputManager.OnPressed(btn, action, async);
+ }
+
+ public static void OnReleasedGlobal(CButton btn, Action action, bool async = false) {
+ globalInputManager.OnReleased(btn, action, async);
+ }
+
+ public static void OnHeldGlobal(CButton btn, Action action, bool async = false) {
+ globalInputManager.OnHeld(btn, action, async);
+ }
+
+ public static void OnHeld(CButton btn, string name, Action action, bool async = false) {
+ if (!inputManagers.ContainsKey(name)) {
+ inputManagers[name] = new InputManager();
+ }
+ inputManagers[name].OnHeld(btn, action, async);
+ }
+
+ public static void OnPressed(CButton btn, string name, Action action, bool async = false) {
+ if (!inputManagers.ContainsKey(name)) {
+ inputManagers[name] = new InputManager();
+ }
+ inputManagers[name].OnPressed(btn, action, async);
+ }
+
+ public static void OnReleased(CButton btn, string name, Action action, bool async = false) {
+ if (!inputManagers.ContainsKey(name)) {
+ inputManagers[name] = new InputManager();
+ }
+ inputManagers[name].OnReleased(btn, action, async);
+ }
+
+ public static InputManager getInputManager(string name) {
+ if (inputManagers.ContainsKey(name)) {
+ return inputManagers[name];
+ }
+ InputManager inputManager = new(name);
+ inputManagers[name] = inputManager;
+ enabled[name] = true;
+ return inputManager;
+ }
+
+ public void setEnabled(bool enabled) {
+ InputManager.enabled[name] = enabled;
+ }
+
+ public static void setEnabled(string name, bool enabled) {
+ if (!inputManagers.ContainsKey(name)) return;
+ InputManager.enabled[name] = enabled;
+ }
+
+ public static void setSoleEnabled(string name) {
+ foreach (string key in inputManagers.Keys) {
+ enabled[key] = key == name;
+ }
+ }
+
+ public static void setAllEnabled(bool enabled) {
+ foreach (string key in inputManagers.Keys) {
+ InputManager.enabled[key] = enabled;
+ }
+ }
+
+ public static List getEnabled() {
+ return inputManagers.Keys.Where(key => enabled[key]).ToList();
+ }
+
+ public static List getDisabled() {
+ return inputManagers.Keys.Where(key => !enabled[key]).ToList();
+ }
+
+ public static void setEnabled(IEnumerable names) {
+ // IEnumerable can be lazy so we need to force it to evaluate
+ // otherwise every time we check name.Contains(key) it will evaluate the whole thing again
+ names = names.ToList();
+ foreach (string key in inputManagers.Keys) {
+ enabled[key] = names.Contains(key);
+ }
+ }
+
+ public static void setDisabled(IEnumerable names) {
+ // IEnumerable can be lazy so we need to force it to evaluate
+ // otherwise every time we check name.Contains(key) it will evaluate the whole thing again
+ names = names.ToList();
+ foreach (string key in inputManagers.Keys) {
+ enabled[key] = !names.Contains(key);
+ }
+ }
+ }
+}
\ No newline at end of file