From e8e17f4f888c30f9682fc2ed81a1cca80897a325 Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:19:22 -0400 Subject: [PATCH 1/8] chore: bump test Godot project to 4.2 --- GodotTestDriver.Tests/project.godot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GodotTestDriver.Tests/project.godot b/GodotTestDriver.Tests/project.godot index 85acf38..9e6fb65 100644 --- a/GodotTestDriver.Tests/project.godot +++ b/GodotTestDriver.Tests/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="GodotTestDriver.Tests" run/main_scene="res://Tests.tscn" -config/features=PackedStringArray("4.0", "C#", "Mobile") +config/features=PackedStringArray("4.2", "C#", "Mobile") config/icon="res://icon.svg" [dotnet] From d1324d65605aefc20e724b3e3655a896ec007a3e Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:26:52 -0400 Subject: [PATCH 2/8] test: make Godot use PascalCase scene filenames --- GodotTestDriver.Tests/project.godot | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GodotTestDriver.Tests/project.godot b/GodotTestDriver.Tests/project.godot index 9e6fb65..5d58d07 100644 --- a/GodotTestDriver.Tests/project.godot +++ b/GodotTestDriver.Tests/project.godot @@ -19,6 +19,10 @@ config/icon="res://icon.svg" project/assembly_name="GodotTestDriver.Tests" +[editor] + +naming/scene_name_casing=1 + [input] test_action={ From 93c09d506a0d511a8da2c548bab24dab4a96918d Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:07:29 -0400 Subject: [PATCH 3/8] refactor: rename existing input extensions Changed names for clarity and in prep for controller input: - KeyboardControlExtensions -> KeyboardInputExtensions - MouseControlExtensions -> MouseInputExtensions - ActionsControlExtensions -> ActionsInputExtensions BREAKING: Changed class names; see above --- .../{ActionsControlExtensions.cs => ActionsInputExtensions.cs} | 2 +- ...{KeyboardControlExtensions.cs => KeyboardInputExtensions.cs} | 2 +- .../{MouseControlExtensions.cs => MouseInputExtensions.cs} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename GodotTestDriver/Input/{ActionsControlExtensions.cs => ActionsInputExtensions.cs} (97%) rename GodotTestDriver/Input/{KeyboardControlExtensions.cs => KeyboardInputExtensions.cs} (98%) rename GodotTestDriver/Input/{MouseControlExtensions.cs => MouseInputExtensions.cs} (98%) diff --git a/GodotTestDriver/Input/ActionsControlExtensions.cs b/GodotTestDriver/Input/ActionsInputExtensions.cs similarity index 97% rename from GodotTestDriver/Input/ActionsControlExtensions.cs rename to GodotTestDriver/Input/ActionsInputExtensions.cs index e4e420f..fc55e02 100644 --- a/GodotTestDriver/Input/ActionsControlExtensions.cs +++ b/GodotTestDriver/Input/ActionsInputExtensions.cs @@ -11,7 +11,7 @@ namespace Chickensoft.GodotTestDriver.Input; /// Input action extensions. /// [PublicAPI] -public static class ActionsControlExtensions +public static class ActionsInputExtensions { /// /// Hold an input action for a given duration. diff --git a/GodotTestDriver/Input/KeyboardControlExtensions.cs b/GodotTestDriver/Input/KeyboardInputExtensions.cs similarity index 98% rename from GodotTestDriver/Input/KeyboardControlExtensions.cs rename to GodotTestDriver/Input/KeyboardInputExtensions.cs index 83f10f1..196bb6c 100644 --- a/GodotTestDriver/Input/KeyboardControlExtensions.cs +++ b/GodotTestDriver/Input/KeyboardInputExtensions.cs @@ -9,7 +9,7 @@ namespace Chickensoft.GodotTestDriver.Input; /// Extensions which allow to send keyboard inputs. /// [PublicAPI] -public static class KeyboardControlExtensions +public static class KeyboardInputExtensions { /// /// Presses the given key with the given modifiers. diff --git a/GodotTestDriver/Input/MouseControlExtensions.cs b/GodotTestDriver/Input/MouseInputExtensions.cs similarity index 98% rename from GodotTestDriver/Input/MouseControlExtensions.cs rename to GodotTestDriver/Input/MouseInputExtensions.cs index e8b2964..771232f 100644 --- a/GodotTestDriver/Input/MouseControlExtensions.cs +++ b/GodotTestDriver/Input/MouseInputExtensions.cs @@ -7,7 +7,7 @@ namespace Chickensoft.GodotTestDriver.Input; /// Extension functionality for controlling the mouse from tests. /// [PublicAPI] -public static class MouseControlExtensions +public static class MouseInputExtensions { /// /// Clicks the mouse at the specified position. From 44bddd3477f96031ccde168aada865b618dacd47 Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:56:26 -0400 Subject: [PATCH 4/8] feat: add controller-input extensions Added a new class with extension methods for simulating input from game controllers. Extension methods fire input events, analogous to keyboard- and mouse-input extensions. Action-based controller inputs can be simulated with existing action-input extensions. --- .../ActionsControlExtensionsTest.cs | 11 + .../ControllerInputExtensionsTest.cs | 231 ++++++++++++++++++ .../ControllerInputExtensionsTest.tscn | 3 + .../Input/ActionsInputExtensions.cs | 8 +- .../Input/ControllerInputExtensions.cs | 166 +++++++++++++ 5 files changed, 416 insertions(+), 3 deletions(-) create mode 100644 GodotTestDriver.Tests/ControllerInputExtensionsTest.cs create mode 100644 GodotTestDriver.Tests/ControllerInputExtensionsTest.tscn create mode 100644 GodotTestDriver/Input/ControllerInputExtensions.cs diff --git a/GodotTestDriver.Tests/ActionsControlExtensionsTest.cs b/GodotTestDriver.Tests/ActionsControlExtensionsTest.cs index cdc5369..7d14c88 100644 --- a/GodotTestDriver.Tests/ActionsControlExtensionsTest.cs +++ b/GodotTestDriver.Tests/ActionsControlExtensionsTest.cs @@ -40,6 +40,17 @@ public void StartActionSetsGlobalActionPressed() RootNode.EndAction(TestAction); } + [Test] + public void StartActionClampsStrengthBetweenZeroAndOne() + { + RootNode.StartAction(TestAction, -1); + Input.GetActionStrength(TestAction).ShouldBe(0); + RootNode.EndAction(TestAction); + RootNode.StartAction(TestAction, 2); + Input.GetActionStrength(TestAction).ShouldBe(1); + RootNode.EndAction(TestAction); + } + [Test] public void EndActionUnsetsGlobalActionPressed() { diff --git a/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs b/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs new file mode 100644 index 0000000..ec93c45 --- /dev/null +++ b/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs @@ -0,0 +1,231 @@ +namespace Chickensoft.GodotTestDriver.Tests; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Chickensoft.GoDotTest; +using Chickensoft.GodotTestDriver.Input; +using Godot; +using JetBrains.Annotations; +using Shouldly; + +[UsedImplicitly] +public partial class ControllerInputExtensionsTest : DriverTest +{ + private class TimedButtonEvent + { + public ulong ProcessFrame { get; set; } + public DateTime DateTime { get; set; } + public InputEventJoypadButton Event { get; set; } + + public TimedButtonEvent(ulong processFrame, DateTime dateTime, InputEventJoypadButton @event) + { + ProcessFrame = processFrame; + DateTime = dateTime; + Event = @event; + } + } + + private partial class JoypadButtonInputEventTestNode : Node + { + public IList Events { get; } = new List(); + + public override void _Input(InputEvent @event) + { + if (@event is InputEventJoypadButton buttonEvent) + { + var frame = Engine.GetProcessFrames(); + var time = DateTime.Now; + Events.Add(new TimedButtonEvent(frame, time, buttonEvent)); + } + } + } + + private class TimedMotionEvent + { + public DateTime DateTime { get; set; } + public InputEventJoypadMotion Event { get; set; } + + public TimedMotionEvent(DateTime dateTime, InputEventJoypadMotion @event) + { + DateTime = dateTime; + Event = @event; + } + } + + private partial class JoypadMotionInputEventTestNode : Node + { + public IList Events { get; } = new List(); + + public override void _Input(InputEvent @event) + { + if (@event is InputEventJoypadMotion buttonEvent) + { + var time = DateTime.Now; + Events.Add(new TimedMotionEvent(time, buttonEvent)); + } + } + } + + public ControllerInputExtensionsTest(Node testScene) : base(testScene) + { + } + + [Test] + public void PressJoypadButtonFiresInputEvent() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Press controller device 1's X button with 80% pressure. Note that this doesn't correspond to + // actual functionality of common gamepads. + var button = JoyButton.X; + var deviceID = 1; + var pressure = 0.8f; + testNode.PressJoypadButton(button, deviceID, pressure); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Pressed.ShouldBeTrue(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(pressure); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public void ReleaseJoypadButtonFiresInputEvent() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Release controller device 1's X button. + var button = JoyButton.X; + var deviceID = 1; + testNode.ReleaseJoypadButton(button, deviceID); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Pressed.ShouldBeFalse(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(0.0f); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public void TapJoypadButtonFiresInputEvents() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Tap controller device 1's X button with 80% pressure. Note that this doesn't correspond to + // actual functionality of common gamepads. + var button = JoyButton.X; + var deviceID = 1; + var pressure = 0.8f; + testNode.TapJoypadButton(button, deviceID, pressure); + testNode.Events.Count.ShouldBe(2); + testNode.Events[0].Event.Pressed.ShouldBeTrue(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(pressure); + testNode.Events[1].Event.Pressed.ShouldBeFalse(); + testNode.Events[1].Event.ButtonIndex.ShouldBe(button); + testNode.Events[1].Event.Device.ShouldBe(deviceID); + testNode.Events[1].Event.Pressure.ShouldBe(0.0f); + testNode.Events[1].ProcessFrame.ShouldBe(testNode.Events[0].ProcessFrame); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public async Task HoldJoypadButtonFiresInputEvents() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Hold controller device 1's X button with 80% pressure for 2 seconds. Note that this doesn't correspond to + // actual functionality of common gamepads. + var button = JoyButton.X; + var deviceID = 1; + var pressure = 0.8f; + var seconds = 0.5f; + var timeTolerance = 0.1f; + await testNode.HoldJoypadButtonFor(seconds, button, deviceID, pressure); + testNode.Events.Count.ShouldBe(2); + testNode.Events[0].Event.Pressed.ShouldBeTrue(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(pressure); + testNode.Events[1].Event.Pressed.ShouldBeFalse(); + testNode.Events[1].Event.ButtonIndex.ShouldBe(button); + testNode.Events[1].Event.Device.ShouldBe(deviceID); + testNode.Events[1].Event.Pressure.ShouldBe(0.0f); + var timeDiff = testNode.Events[1].DateTime - testNode.Events[0].DateTime; + timeDiff.TotalSeconds.ShouldBe(seconds, timeTolerance); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public void MoveJoypadAxisFiresInputEvent() + { + var testNode = new JoypadMotionInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Move controller device 1's right-thumbstick x-axis to the -0.3 position (about 1/3 left stick). + var axis = JoyAxis.RightX; + var deviceID = 1; + var position = -0.3f; + testNode.MoveJoypadAxisTo(axis, position, deviceID); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Axis.ShouldBe(axis); + testNode.Events[0].Event.AxisValue.ShouldBe(position); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + RootNode.RemoveChild(testNode); + testNode.QueueFree(); + } + + [Test] + public void ReleaseJoypadAxisFiresInputEvent() + { + var testNode = new JoypadMotionInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Move controller device 1's right-thumbstick x-axis to the rest position. + var axis = JoyAxis.RightX; + var deviceID = 1; + testNode.ReleaseJoypadAxis(axis, deviceID); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Axis.ShouldBe(axis); + testNode.Events[0].Event.AxisValue.ShouldBe(0.0f); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + RootNode.RemoveChild(testNode); + testNode.QueueFree(); + } + + [Test] + public async Task HoldJoypadAxisFiresInputEvents() + { + var testNode = new JoypadMotionInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Move controller device 1's right-thumbstick x-axis to the rest position. + var axis = JoyAxis.RightX; + var deviceID = 1; + var position = -0.3f; + var seconds = 0.5f; + var timeTolerance = 0.1f; + await testNode.HoldJoypadAxisFor(seconds, axis, position, deviceID); + testNode.Events.Count.ShouldBe(2); + testNode.Events[0].Event.Axis.ShouldBe(axis); + testNode.Events[0].Event.AxisValue.ShouldBe(position); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[1].Event.Axis.ShouldBe(axis); + testNode.Events[1].Event.AxisValue.ShouldBe(0.0f); + testNode.Events[1].Event.Device.ShouldBe(deviceID); + var timeDiff = testNode.Events[1].DateTime - testNode.Events[0].DateTime; + timeDiff.TotalSeconds.ShouldBe(seconds, timeTolerance); + RootNode.RemoveChild(testNode); + testNode.QueueFree(); + } +} diff --git a/GodotTestDriver.Tests/ControllerInputExtensionsTest.tscn b/GodotTestDriver.Tests/ControllerInputExtensionsTest.tscn new file mode 100644 index 0000000..30e30c9 --- /dev/null +++ b/GodotTestDriver.Tests/ControllerInputExtensionsTest.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://blvfape3o5unh"] + +[node name="ControllerInputExtensionsTest" type="Node"] diff --git a/GodotTestDriver/Input/ActionsInputExtensions.cs b/GodotTestDriver/Input/ActionsInputExtensions.cs index fc55e02..bf8182f 100644 --- a/GodotTestDriver/Input/ActionsInputExtensions.cs +++ b/GodotTestDriver/Input/ActionsInputExtensions.cs @@ -1,5 +1,6 @@ -namespace Chickensoft.GodotTestDriver.Input; - +namespace Chickensoft.GodotTestDriver.Input; + +using System; using System.Threading.Tasks; using Godot; using GodotTestDriver.Util; @@ -46,7 +47,8 @@ public static void StartAction( Action = actionName, Pressed = true }); - Input.ActionPress(actionName, strength); + // clamp value ourselves to work around godotengine/godot/issues/89945 + Input.ActionPress(actionName, Math.Clamp(strength, 0f, 1f)); Input.FlushBufferedEvents(); } diff --git a/GodotTestDriver/Input/ControllerInputExtensions.cs b/GodotTestDriver/Input/ControllerInputExtensions.cs new file mode 100644 index 0000000..ed504dc --- /dev/null +++ b/GodotTestDriver/Input/ControllerInputExtensions.cs @@ -0,0 +1,166 @@ +namespace Chickensoft.GodotTestDriver.Input; + +using System.Threading.Tasks; +using Chickensoft.GodotTestDriver.Util; +using Godot; +using JetBrains.Annotations; + +/// +/// Extensions for simulating controller inputs. +/// +/// +[PublicAPI] +public static class ControllerInputExtensions +{ + /// + /// Holds a controller axis at a given position for a given period of time before releasing + /// it, causing events to fire at an appropriate interval. + /// + /// + /// Does not affect the values of , + /// , or . + /// + /// Node that generates simulated input. + /// Input duration, in seconds. + /// The controller axis to set. + /// The axis position, in the range -1.0f to 1.0f. + /// Input device that is the source of the event. + /// Asynchronous task completed when the button is released. + /// + public static async Task HoldJoypadAxisFor(this Node node, float seconds, JoyAxis axis, float axisValue, int device = 0) + { + node.MoveJoypadAxisTo(axis, axisValue, device); + await node.Wait(seconds); + node.ReleaseJoypadAxis(axis, device); + } + + /// + /// Holds a controller button down for a given period of time before releasing it, causing + /// events to fire at an appropriate interval. + /// + /// + /// Does not affect the value of + /// or . + /// + /// Node that generates simulated input. + /// Input duration, in seconds. + /// Button that will be pressed. + /// Input device that is the source of the event. + /// Pressure on the button, in the range 0.0f to 1.0f. + /// Asynchronous task completed when the button is released. + public static async Task HoldJoypadButtonFor(this Node node, float seconds, JoyButton buttonIndex, int device = 0, float pressure = 1.0f) + { + node.PressJoypadButton(buttonIndex, device, pressure); + await node.Wait(seconds); + node.ReleaseJoypadButton(buttonIndex, device); + } + + /// + /// Presses and releases a controller button, causing + /// events to fire. + /// + /// + /// Does not affect the value of + /// or . + /// + /// Node that generates simulated input. + /// Button that will be pressed. + /// Input device that is the source of the event. + /// Pressure on the button, in the range 0.0f to 1.0f. + public static void TapJoypadButton(this Node node, JoyButton buttonIndex, int device = 0, float pressure = 1.0f) + { + node.PressJoypadButton(buttonIndex, device, pressure); + node.ReleaseJoypadButton(buttonIndex, device); + } + + /// + /// Set a controller axis to a given position, causing a + /// to fire. + /// + /// + /// Although the full valid range of is -1.0f to 1.0f, + /// some controller axes (e.g., gamepad triggers) only generate values between 0.0f + /// and 1.0f. Does not affect the values of , + /// , or . + /// + /// Node that generates simulated input. + /// The controller axis to set. + /// The axis position, in the range -1.0f to 1.0f. + /// Input device that is the source of the event. + public static void MoveJoypadAxisTo(this Node _, JoyAxis axis, float axisValue, int device = 0) + { + var inputEvent = new InputEventJoypadMotion + { + Axis = axis, + AxisValue = axisValue, + Device = device + }; + Input.ParseInputEvent(inputEvent); + Input.FlushBufferedEvents(); + } + + /// + /// Release a controller axis, setting it to its rest position, causing a + /// to fire. + /// + /// + /// Equivalent to node.MoveJoypadAxisTo(axis, 0.0f, device). Does not affect + /// the values of , + /// , or . + /// + /// Node that generates simulated input. + /// The controller axis to release. + /// Input device that is the source of the event. + /// + public static void ReleaseJoypadAxis(this Node node, JoyAxis axis, int device = 0) + { + node.MoveJoypadAxisTo(axis, 0.0f, device); + } + + /// + /// Presses a controller button, causing an to fire. + /// + /// + /// Does not affect the values of + /// or . + /// + /// Node that generates simulated input. + /// Button that will be pressed. + /// Input device that is the source of the event. + /// Pressure on the button, in the range 0.0f to 1.0f. + public static void PressJoypadButton(this Node _, JoyButton buttonIndex, int device = 0, float pressure = 1.0f) + { + var inputEvent = new InputEventJoypadButton + { + Pressed = true, + ButtonIndex = buttonIndex, + Pressure = pressure, + Device = device + }; + Input.ParseInputEvent(inputEvent); + Input.FlushBufferedEvents(); + } + + /// + /// Releases a controller button, causing an to fire. + /// + /// + /// Does not affect the values of + /// or . + /// + /// Node that generates simulated input. + /// Button that will be released. + /// Input device that is the source of the event. + public static void ReleaseJoypadButton(this Node _, JoyButton buttonIndex, int device = 0) + { + var inputEvent = new InputEventJoypadButton + { + Pressed = false, + ButtonIndex = buttonIndex, + Pressure = 0.0f, + Device = device + }; + Input.ParseInputEvent(inputEvent); + Input.FlushBufferedEvents(); + } +} From 9db012a9a053071d5a65668d7c25a56942cbd092 Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:22:47 -0400 Subject: [PATCH 5/8] test: rename action-input tests to match source Renamed: - ActionsControlExtensionsTests -> ActionsInputExtensionsTests to match new naming scheme for input extension classes. --- ...ControlExtensionsTest.cs => ActionsInputExtensionsTest.cs} | 4 ++-- ...rolExtensionsTest.tscn => ActionsInputExtensionsTest.tscn} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename GodotTestDriver.Tests/{ActionsControlExtensionsTest.cs => ActionsInputExtensionsTest.cs} (93%) rename GodotTestDriver.Tests/{ActionsControlExtensionsTest.tscn => ActionsInputExtensionsTest.tscn} (100%) diff --git a/GodotTestDriver.Tests/ActionsControlExtensionsTest.cs b/GodotTestDriver.Tests/ActionsInputExtensionsTest.cs similarity index 93% rename from GodotTestDriver.Tests/ActionsControlExtensionsTest.cs rename to GodotTestDriver.Tests/ActionsInputExtensionsTest.cs index 7d14c88..8d457fd 100644 --- a/GodotTestDriver.Tests/ActionsControlExtensionsTest.cs +++ b/GodotTestDriver.Tests/ActionsInputExtensionsTest.cs @@ -7,7 +7,7 @@ namespace Chickensoft.GodotTestDriver.Tests; using Shouldly; [UsedImplicitly] -public partial class ActionsControlExtensionsTest : DriverTest +public partial class ActionsInputExtensionsTest : DriverTest { private partial class ActionInputEventTestNode : Node { @@ -27,7 +27,7 @@ public override void _Input(InputEvent @event) private const string TestAction = "test_action"; - public ActionsControlExtensionsTest(Node testScene) : base(testScene) + public ActionsInputExtensionsTest(Node testScene) : base(testScene) { } diff --git a/GodotTestDriver.Tests/ActionsControlExtensionsTest.tscn b/GodotTestDriver.Tests/ActionsInputExtensionsTest.tscn similarity index 100% rename from GodotTestDriver.Tests/ActionsControlExtensionsTest.tscn rename to GodotTestDriver.Tests/ActionsInputExtensionsTest.tscn From 22269f61f289e6b150d3450c88b63b968b53a55c Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:10:59 -0400 Subject: [PATCH 6/8] docs: regularize input-extension docs Improved similarity of formatting for XML docs on input-extension classes and methods. --- GodotTestDriver/Input/ActionsInputExtensions.cs | 8 ++++---- GodotTestDriver/Input/KeyboardInputExtensions.cs | 10 +++++----- GodotTestDriver/Input/MouseInputExtensions.cs | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/GodotTestDriver/Input/ActionsInputExtensions.cs b/GodotTestDriver/Input/ActionsInputExtensions.cs index bf8182f..b59027c 100644 --- a/GodotTestDriver/Input/ActionsInputExtensions.cs +++ b/GodotTestDriver/Input/ActionsInputExtensions.cs @@ -9,7 +9,7 @@ namespace Chickensoft.GodotTestDriver.Input; #pragma warning disable IDE0060 /// -/// Input action extensions. +/// Extensions for simulating action inputs. /// [PublicAPI] public static class ActionsInputExtensions @@ -17,7 +17,7 @@ public static class ActionsInputExtensions /// /// Hold an input action for a given duration. /// - /// Node to supply input to. + /// Node that generates simulated input. /// Time, in seconds. /// Name of the action. /// Task that completes when the input finishes. @@ -35,7 +35,7 @@ string actionName /// /// Start an input action. /// - /// Node to supply input to. + /// Node that generates simulated input. /// Name of the action. /// Action strength (optional — default is 1.0). public static void StartAction( @@ -55,7 +55,7 @@ public static void StartAction( /// /// End an input action. /// - /// Node to supply input to. + /// Node that generates simulated input. /// Name of the action. public static void EndAction(this Node node, string actionName) { diff --git a/GodotTestDriver/Input/KeyboardInputExtensions.cs b/GodotTestDriver/Input/KeyboardInputExtensions.cs index 196bb6c..6f6f951 100644 --- a/GodotTestDriver/Input/KeyboardInputExtensions.cs +++ b/GodotTestDriver/Input/KeyboardInputExtensions.cs @@ -6,7 +6,7 @@ namespace Chickensoft.GodotTestDriver.Input; using JetBrains.Annotations; /// -/// Extensions which allow to send keyboard inputs. +/// Extensions for simulating keyboard inputs. /// [PublicAPI] public static class KeyboardInputExtensions @@ -14,7 +14,7 @@ public static class KeyboardInputExtensions /// /// Presses the given key with the given modifiers. /// - /// Node to perform input on. + /// Node that generates simulated input. /// Keyboard key. /// Control modifier. /// Alt modifier. @@ -46,7 +46,7 @@ public static void PressKey( /// /// Simulate a key being pressed for a certain amount of time. /// - /// Node to perform input on. + /// Node that generates simulated input. /// Input duration, in seconds. /// Keyboard key. /// Control modifier. @@ -72,7 +72,7 @@ public static async Task HoldKeyFor( /// /// Releases the given key with the given modifier state. /// - /// Node to perform input on. + /// Node that generates simulated input. /// Keyboard key. /// Control modifier. /// Alt modifier. @@ -104,7 +104,7 @@ public static void ReleaseKey( /// /// Presses and releases a key with the given modifiers. /// - /// Node to perform input on. + /// Node that generates simulated input. /// Keyboard key. /// Control modifier. /// Alt modifier. diff --git a/GodotTestDriver/Input/MouseInputExtensions.cs b/GodotTestDriver/Input/MouseInputExtensions.cs index 771232f..58461f5 100644 --- a/GodotTestDriver/Input/MouseInputExtensions.cs +++ b/GodotTestDriver/Input/MouseInputExtensions.cs @@ -4,7 +4,7 @@ namespace Chickensoft.GodotTestDriver.Input; using JetBrains.Annotations; /// -/// Extension functionality for controlling the mouse from tests. +/// Extensions for simulating mouse inputs. /// [PublicAPI] public static class MouseInputExtensions @@ -12,7 +12,7 @@ public static class MouseInputExtensions /// /// Clicks the mouse at the specified position. /// - /// Viewport. + /// Viewport that generates simulated input. /// Position, in viewport coordinates. /// Mouse button. public static void ClickMouseAt(this Viewport viewport, Vector2 position, MouseButton button = MouseButton.Left) @@ -24,7 +24,7 @@ public static void ClickMouseAt(this Viewport viewport, Vector2 position, MouseB /// /// Moves the mouse to the specified position. /// - /// Viewport. + /// Viewport that generates simulated input. /// Position, in viewport coordinates. public static void MoveMouseTo(this Viewport viewport, Vector2 position) { @@ -43,7 +43,7 @@ public static void MoveMouseTo(this Viewport viewport, Vector2 position) /// /// Drags the mouse from the start position to the end position. /// - /// Viewport. + /// Viewport that generates simulated input. /// Start position, in viewport coordinates. /// End position, in viewport coordinates. /// Mouse button. @@ -56,7 +56,7 @@ public static void DragMouse(this Viewport viewport, Vector2 start, Vector2 end, /// /// Presses the given mouse button. /// - /// Viewport. + /// Viewport that generates simulated input. /// Mouse button (left by default). public static void PressMouse(this Viewport _, MouseButton button = MouseButton.Left) { @@ -72,7 +72,7 @@ public static void PressMouse(this Viewport _, MouseButton button = MouseButton. /// /// Releases the given mouse button. /// - /// Viewport. + /// Viewport that generates simulated input. /// Mouse button (left by default). public static void ReleaseMouse(this Viewport _, MouseButton button = MouseButton.Left) { From 2fc526e1dc0553d2f4676cab2279c9526db788a2 Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:37:33 -0400 Subject: [PATCH 7/8] docs: update README for controller input * Provided section on extension methods to describe event-based input simulation * Provided pointer to action-simulation section for simulating controller actions --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index 3382226..2cad144 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,51 @@ node.ReleaseKey(KeyList.A); node.TypeKey(KeyList.A); ``` +### Simulating controller input + +GodotTest provides a number of extension functions on `SceneTree`/`Node` that allow you to simulate controller input using Godot's [`InputEventJoypadButton`](https://docs.godotengine.org/en/stable/classes/class_inputeventjoypadbutton.html#class-inputeventjoypadbutton) and [`InputEventJoypadMotion`](https://docs.godotengine.org/en/stable/classes/class_inputeventjoypadmotion.html#class-inputeventjoypadmotion) events. + +```csharp +// you can press down a controller button +node.PressJoypadButton(JoyButton.Y); + +// you can release a controller button +node.ReleaseJoypadButton(JoyButton.Y); + +// you can specify a particular controller device +var deviceID = 0; +node.PressJoypadButton(JoyButton.Y, deviceID); +node.ReleaseJoypadButton(JoyButton.Y, deviceID); + +// you can simulate pressure for pressure-sensitive devices +var pressure = 0.8f; +node.PressJoypadButton(JoyButton.Y, deviceID, pressure); +node.ReleaseJoypadButton(JoyButton.Y, deviceID); + +// you can combine pressing and releasing a button +node.TapJoypadButton(JoyButton.Y, deviceID, pressure); + +// you can move an analog controller axis to a given position, with 0 being the rest position +// for instance: +// * a gamepad trigger will range from 0 to 1 +// * a thumbstick's x-axis will range from -1 to 1 +node.MoveJoypadAxisTo(JoyAxis.RightX, -0.3f); + +// you can release a controller axis (equivalent to setting its position to 0) +node.ReleaseJoypadAxis(JoyAxis.RightX); + +// you can specify a particular controller device +node.MoveJoypadAxisTo(JoyAxis.RightX, -0.3f, deviceID); +node.ReleaseJoypadAxis(JoyAxis.RightX, deviceID); + +// hold a controller button for 1.5 seconds +await node.HoldJoypadButtonFor(1.5f, JoyButton.Y, deviceID, pressure); +// hold a controller axis position for 1.5 seconds +await node.HoldJoypadAxisFor(1.5f, JoyAxis.RightX, -0.3f, deviceID); +``` + +To simulate [controller input using mapped actions](https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html#which-input-singleton-method-should-i-use), for use with Godot's `Input.GetActionStrength()`, `Input.GetAxis()`, and `Input.GetVector()` methods, see the next section. + ### Simulating other actions Since version 2.1.0 you can now also simulate actions like this: From e01444d7f7d4dc14c46cad49efc173b8c03a97c2 Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:40:48 -0400 Subject: [PATCH 8/8] chore: fix line endings and make LF default Updated editorconfig to use LF for all file types unless overridden (e.g., for bat/cmd files). --- .editorconfig | 1 + .../ActionsInputExtensionsTest.cs | 220 ++++----- .../ControllerInputExtensionsTest.cs | 462 +++++++++--------- .../Input/ActionsInputExtensions.cs | 4 +- 4 files changed, 344 insertions(+), 343 deletions(-) diff --git a/.editorconfig b/.editorconfig index e1fa4e9..271cfe8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,6 +18,7 @@ indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true +end_of_line = lf ########################################## # File Extension Settings diff --git a/GodotTestDriver.Tests/ActionsInputExtensionsTest.cs b/GodotTestDriver.Tests/ActionsInputExtensionsTest.cs index 8d457fd..2858fb5 100644 --- a/GodotTestDriver.Tests/ActionsInputExtensionsTest.cs +++ b/GodotTestDriver.Tests/ActionsInputExtensionsTest.cs @@ -1,110 +1,110 @@ -namespace Chickensoft.GodotTestDriver.Tests; - -using Chickensoft.GoDotTest; -using Godot; -using GodotTestDriver.Input; -using JetBrains.Annotations; -using Shouldly; - -[UsedImplicitly] -public partial class ActionsInputExtensionsTest : DriverTest -{ - private partial class ActionInputEventTestNode : Node - { - public bool HasInputEventFired { get; set; } - public bool WasInputPressed { get; set; } - public StringName InputEventName { get; set; } = string.Empty; - - public override void _Input(InputEvent @event) - { - if (@event.IsAction(InputEventName)) - { - HasInputEventFired = true; - WasInputPressed = @event.IsActionPressed(InputEventName); - } - } - } - - private const string TestAction = "test_action"; - - public ActionsInputExtensionsTest(Node testScene) : base(testScene) - { - } - - [Test] - public void StartActionSetsGlobalActionPressed() - { - Input.IsActionPressed(TestAction).ShouldBeFalse(); - RootNode.StartAction(TestAction); - Input.IsActionPressed(TestAction).ShouldBeTrue(); - RootNode.EndAction(TestAction); - } - - [Test] - public void StartActionClampsStrengthBetweenZeroAndOne() - { - RootNode.StartAction(TestAction, -1); - Input.GetActionStrength(TestAction).ShouldBe(0); - RootNode.EndAction(TestAction); - RootNode.StartAction(TestAction, 2); - Input.GetActionStrength(TestAction).ShouldBe(1); - RootNode.EndAction(TestAction); - } - - [Test] - public void EndActionUnsetsGlobalActionPressed() - { - RootNode.StartAction(TestAction); - RootNode.EndAction(TestAction); - Input.IsActionPressed(TestAction).ShouldBeFalse(); - } - - [Test] - public void StartActionSetsGlobalActionJustPressed() - { - RootNode.StartAction(TestAction); - Input.IsActionJustPressed(TestAction).ShouldBeTrue(); - RootNode.EndAction(TestAction); - } - - [Test] - public void EndActionSetsGlobalActionJustReleased() - { - RootNode.StartAction(TestAction); - RootNode.EndAction(TestAction); - Input.IsActionJustReleased(TestAction).ShouldBeTrue(); - } - - [Test] - public void StartActionSendsInputEvent() - { - var inputTestNode = new ActionInputEventTestNode - { - InputEventName = TestAction - }; - RootNode.AddChild(inputTestNode); - inputTestNode.HasInputEventFired.ShouldBeFalse(); - inputTestNode.StartAction(TestAction); - inputTestNode.HasInputEventFired.ShouldBeTrue(); - inputTestNode.WasInputPressed.ShouldBeTrue(); - inputTestNode.EndAction(TestAction); - RootNode.RemoveChild(inputTestNode); // Remove immediately since we won't wait a frame for the free - inputTestNode.QueueFree(); - } - - [Test] - public void EndActionSendsInputEvent() - { - var inputTestNode = new ActionInputEventTestNode - { - InputEventName = TestAction - }; - RootNode.AddChild(inputTestNode); - inputTestNode.HasInputEventFired.ShouldBeFalse(); - inputTestNode.EndAction(TestAction); - inputTestNode.HasInputEventFired.ShouldBeTrue(); - inputTestNode.WasInputPressed.ShouldBeFalse(); - RootNode.RemoveChild(inputTestNode); // Remove immediately since we won't wait a frame for the free - inputTestNode.QueueFree(); - } -} +namespace Chickensoft.GodotTestDriver.Tests; + +using Chickensoft.GoDotTest; +using Godot; +using GodotTestDriver.Input; +using JetBrains.Annotations; +using Shouldly; + +[UsedImplicitly] +public partial class ActionsInputExtensionsTest : DriverTest +{ + private partial class ActionInputEventTestNode : Node + { + public bool HasInputEventFired { get; set; } + public bool WasInputPressed { get; set; } + public StringName InputEventName { get; set; } = string.Empty; + + public override void _Input(InputEvent @event) + { + if (@event.IsAction(InputEventName)) + { + HasInputEventFired = true; + WasInputPressed = @event.IsActionPressed(InputEventName); + } + } + } + + private const string TestAction = "test_action"; + + public ActionsInputExtensionsTest(Node testScene) : base(testScene) + { + } + + [Test] + public void StartActionSetsGlobalActionPressed() + { + Input.IsActionPressed(TestAction).ShouldBeFalse(); + RootNode.StartAction(TestAction); + Input.IsActionPressed(TestAction).ShouldBeTrue(); + RootNode.EndAction(TestAction); + } + + [Test] + public void StartActionClampsStrengthBetweenZeroAndOne() + { + RootNode.StartAction(TestAction, -1); + Input.GetActionStrength(TestAction).ShouldBe(0); + RootNode.EndAction(TestAction); + RootNode.StartAction(TestAction, 2); + Input.GetActionStrength(TestAction).ShouldBe(1); + RootNode.EndAction(TestAction); + } + + [Test] + public void EndActionUnsetsGlobalActionPressed() + { + RootNode.StartAction(TestAction); + RootNode.EndAction(TestAction); + Input.IsActionPressed(TestAction).ShouldBeFalse(); + } + + [Test] + public void StartActionSetsGlobalActionJustPressed() + { + RootNode.StartAction(TestAction); + Input.IsActionJustPressed(TestAction).ShouldBeTrue(); + RootNode.EndAction(TestAction); + } + + [Test] + public void EndActionSetsGlobalActionJustReleased() + { + RootNode.StartAction(TestAction); + RootNode.EndAction(TestAction); + Input.IsActionJustReleased(TestAction).ShouldBeTrue(); + } + + [Test] + public void StartActionSendsInputEvent() + { + var inputTestNode = new ActionInputEventTestNode + { + InputEventName = TestAction + }; + RootNode.AddChild(inputTestNode); + inputTestNode.HasInputEventFired.ShouldBeFalse(); + inputTestNode.StartAction(TestAction); + inputTestNode.HasInputEventFired.ShouldBeTrue(); + inputTestNode.WasInputPressed.ShouldBeTrue(); + inputTestNode.EndAction(TestAction); + RootNode.RemoveChild(inputTestNode); // Remove immediately since we won't wait a frame for the free + inputTestNode.QueueFree(); + } + + [Test] + public void EndActionSendsInputEvent() + { + var inputTestNode = new ActionInputEventTestNode + { + InputEventName = TestAction + }; + RootNode.AddChild(inputTestNode); + inputTestNode.HasInputEventFired.ShouldBeFalse(); + inputTestNode.EndAction(TestAction); + inputTestNode.HasInputEventFired.ShouldBeTrue(); + inputTestNode.WasInputPressed.ShouldBeFalse(); + RootNode.RemoveChild(inputTestNode); // Remove immediately since we won't wait a frame for the free + inputTestNode.QueueFree(); + } +} diff --git a/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs b/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs index ec93c45..55f1365 100644 --- a/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs +++ b/GodotTestDriver.Tests/ControllerInputExtensionsTest.cs @@ -1,231 +1,231 @@ -namespace Chickensoft.GodotTestDriver.Tests; - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Chickensoft.GoDotTest; -using Chickensoft.GodotTestDriver.Input; -using Godot; -using JetBrains.Annotations; -using Shouldly; - -[UsedImplicitly] -public partial class ControllerInputExtensionsTest : DriverTest -{ - private class TimedButtonEvent - { - public ulong ProcessFrame { get; set; } - public DateTime DateTime { get; set; } - public InputEventJoypadButton Event { get; set; } - - public TimedButtonEvent(ulong processFrame, DateTime dateTime, InputEventJoypadButton @event) - { - ProcessFrame = processFrame; - DateTime = dateTime; - Event = @event; - } - } - - private partial class JoypadButtonInputEventTestNode : Node - { - public IList Events { get; } = new List(); - - public override void _Input(InputEvent @event) - { - if (@event is InputEventJoypadButton buttonEvent) - { - var frame = Engine.GetProcessFrames(); - var time = DateTime.Now; - Events.Add(new TimedButtonEvent(frame, time, buttonEvent)); - } - } - } - - private class TimedMotionEvent - { - public DateTime DateTime { get; set; } - public InputEventJoypadMotion Event { get; set; } - - public TimedMotionEvent(DateTime dateTime, InputEventJoypadMotion @event) - { - DateTime = dateTime; - Event = @event; - } - } - - private partial class JoypadMotionInputEventTestNode : Node - { - public IList Events { get; } = new List(); - - public override void _Input(InputEvent @event) - { - if (@event is InputEventJoypadMotion buttonEvent) - { - var time = DateTime.Now; - Events.Add(new TimedMotionEvent(time, buttonEvent)); - } - } - } - - public ControllerInputExtensionsTest(Node testScene) : base(testScene) - { - } - - [Test] - public void PressJoypadButtonFiresInputEvent() - { - var testNode = new JoypadButtonInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Press controller device 1's X button with 80% pressure. Note that this doesn't correspond to - // actual functionality of common gamepads. - var button = JoyButton.X; - var deviceID = 1; - var pressure = 0.8f; - testNode.PressJoypadButton(button, deviceID, pressure); - testNode.Events.Count.ShouldBe(1); - testNode.Events[0].Event.Pressed.ShouldBeTrue(); - testNode.Events[0].Event.ButtonIndex.ShouldBe(button); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - testNode.Events[0].Event.Pressure.ShouldBe(pressure); - RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free - testNode.QueueFree(); - } - - [Test] - public void ReleaseJoypadButtonFiresInputEvent() - { - var testNode = new JoypadButtonInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Release controller device 1's X button. - var button = JoyButton.X; - var deviceID = 1; - testNode.ReleaseJoypadButton(button, deviceID); - testNode.Events.Count.ShouldBe(1); - testNode.Events[0].Event.Pressed.ShouldBeFalse(); - testNode.Events[0].Event.ButtonIndex.ShouldBe(button); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - testNode.Events[0].Event.Pressure.ShouldBe(0.0f); - RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free - testNode.QueueFree(); - } - - [Test] - public void TapJoypadButtonFiresInputEvents() - { - var testNode = new JoypadButtonInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Tap controller device 1's X button with 80% pressure. Note that this doesn't correspond to - // actual functionality of common gamepads. - var button = JoyButton.X; - var deviceID = 1; - var pressure = 0.8f; - testNode.TapJoypadButton(button, deviceID, pressure); - testNode.Events.Count.ShouldBe(2); - testNode.Events[0].Event.Pressed.ShouldBeTrue(); - testNode.Events[0].Event.ButtonIndex.ShouldBe(button); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - testNode.Events[0].Event.Pressure.ShouldBe(pressure); - testNode.Events[1].Event.Pressed.ShouldBeFalse(); - testNode.Events[1].Event.ButtonIndex.ShouldBe(button); - testNode.Events[1].Event.Device.ShouldBe(deviceID); - testNode.Events[1].Event.Pressure.ShouldBe(0.0f); - testNode.Events[1].ProcessFrame.ShouldBe(testNode.Events[0].ProcessFrame); - RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free - testNode.QueueFree(); - } - - [Test] - public async Task HoldJoypadButtonFiresInputEvents() - { - var testNode = new JoypadButtonInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Hold controller device 1's X button with 80% pressure for 2 seconds. Note that this doesn't correspond to - // actual functionality of common gamepads. - var button = JoyButton.X; - var deviceID = 1; - var pressure = 0.8f; - var seconds = 0.5f; - var timeTolerance = 0.1f; - await testNode.HoldJoypadButtonFor(seconds, button, deviceID, pressure); - testNode.Events.Count.ShouldBe(2); - testNode.Events[0].Event.Pressed.ShouldBeTrue(); - testNode.Events[0].Event.ButtonIndex.ShouldBe(button); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - testNode.Events[0].Event.Pressure.ShouldBe(pressure); - testNode.Events[1].Event.Pressed.ShouldBeFalse(); - testNode.Events[1].Event.ButtonIndex.ShouldBe(button); - testNode.Events[1].Event.Device.ShouldBe(deviceID); - testNode.Events[1].Event.Pressure.ShouldBe(0.0f); - var timeDiff = testNode.Events[1].DateTime - testNode.Events[0].DateTime; - timeDiff.TotalSeconds.ShouldBe(seconds, timeTolerance); - RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free - testNode.QueueFree(); - } - - [Test] - public void MoveJoypadAxisFiresInputEvent() - { - var testNode = new JoypadMotionInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Move controller device 1's right-thumbstick x-axis to the -0.3 position (about 1/3 left stick). - var axis = JoyAxis.RightX; - var deviceID = 1; - var position = -0.3f; - testNode.MoveJoypadAxisTo(axis, position, deviceID); - testNode.Events.Count.ShouldBe(1); - testNode.Events[0].Event.Axis.ShouldBe(axis); - testNode.Events[0].Event.AxisValue.ShouldBe(position); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - RootNode.RemoveChild(testNode); - testNode.QueueFree(); - } - - [Test] - public void ReleaseJoypadAxisFiresInputEvent() - { - var testNode = new JoypadMotionInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Move controller device 1's right-thumbstick x-axis to the rest position. - var axis = JoyAxis.RightX; - var deviceID = 1; - testNode.ReleaseJoypadAxis(axis, deviceID); - testNode.Events.Count.ShouldBe(1); - testNode.Events[0].Event.Axis.ShouldBe(axis); - testNode.Events[0].Event.AxisValue.ShouldBe(0.0f); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - RootNode.RemoveChild(testNode); - testNode.QueueFree(); - } - - [Test] - public async Task HoldJoypadAxisFiresInputEvents() - { - var testNode = new JoypadMotionInputEventTestNode(); - RootNode.AddChild(testNode); - testNode.Events.Count.ShouldBe(0); - // Move controller device 1's right-thumbstick x-axis to the rest position. - var axis = JoyAxis.RightX; - var deviceID = 1; - var position = -0.3f; - var seconds = 0.5f; - var timeTolerance = 0.1f; - await testNode.HoldJoypadAxisFor(seconds, axis, position, deviceID); - testNode.Events.Count.ShouldBe(2); - testNode.Events[0].Event.Axis.ShouldBe(axis); - testNode.Events[0].Event.AxisValue.ShouldBe(position); - testNode.Events[0].Event.Device.ShouldBe(deviceID); - testNode.Events[1].Event.Axis.ShouldBe(axis); - testNode.Events[1].Event.AxisValue.ShouldBe(0.0f); - testNode.Events[1].Event.Device.ShouldBe(deviceID); - var timeDiff = testNode.Events[1].DateTime - testNode.Events[0].DateTime; - timeDiff.TotalSeconds.ShouldBe(seconds, timeTolerance); - RootNode.RemoveChild(testNode); - testNode.QueueFree(); - } -} +namespace Chickensoft.GodotTestDriver.Tests; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Chickensoft.GoDotTest; +using Chickensoft.GodotTestDriver.Input; +using Godot; +using JetBrains.Annotations; +using Shouldly; + +[UsedImplicitly] +public partial class ControllerInputExtensionsTest : DriverTest +{ + private class TimedButtonEvent + { + public ulong ProcessFrame { get; set; } + public DateTime DateTime { get; set; } + public InputEventJoypadButton Event { get; set; } + + public TimedButtonEvent(ulong processFrame, DateTime dateTime, InputEventJoypadButton @event) + { + ProcessFrame = processFrame; + DateTime = dateTime; + Event = @event; + } + } + + private partial class JoypadButtonInputEventTestNode : Node + { + public IList Events { get; } = new List(); + + public override void _Input(InputEvent @event) + { + if (@event is InputEventJoypadButton buttonEvent) + { + var frame = Engine.GetProcessFrames(); + var time = DateTime.Now; + Events.Add(new TimedButtonEvent(frame, time, buttonEvent)); + } + } + } + + private class TimedMotionEvent + { + public DateTime DateTime { get; set; } + public InputEventJoypadMotion Event { get; set; } + + public TimedMotionEvent(DateTime dateTime, InputEventJoypadMotion @event) + { + DateTime = dateTime; + Event = @event; + } + } + + private partial class JoypadMotionInputEventTestNode : Node + { + public IList Events { get; } = new List(); + + public override void _Input(InputEvent @event) + { + if (@event is InputEventJoypadMotion buttonEvent) + { + var time = DateTime.Now; + Events.Add(new TimedMotionEvent(time, buttonEvent)); + } + } + } + + public ControllerInputExtensionsTest(Node testScene) : base(testScene) + { + } + + [Test] + public void PressJoypadButtonFiresInputEvent() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Press controller device 1's X button with 80% pressure. Note that this doesn't correspond to + // actual functionality of common gamepads. + var button = JoyButton.X; + var deviceID = 1; + var pressure = 0.8f; + testNode.PressJoypadButton(button, deviceID, pressure); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Pressed.ShouldBeTrue(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(pressure); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public void ReleaseJoypadButtonFiresInputEvent() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Release controller device 1's X button. + var button = JoyButton.X; + var deviceID = 1; + testNode.ReleaseJoypadButton(button, deviceID); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Pressed.ShouldBeFalse(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(0.0f); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public void TapJoypadButtonFiresInputEvents() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Tap controller device 1's X button with 80% pressure. Note that this doesn't correspond to + // actual functionality of common gamepads. + var button = JoyButton.X; + var deviceID = 1; + var pressure = 0.8f; + testNode.TapJoypadButton(button, deviceID, pressure); + testNode.Events.Count.ShouldBe(2); + testNode.Events[0].Event.Pressed.ShouldBeTrue(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(pressure); + testNode.Events[1].Event.Pressed.ShouldBeFalse(); + testNode.Events[1].Event.ButtonIndex.ShouldBe(button); + testNode.Events[1].Event.Device.ShouldBe(deviceID); + testNode.Events[1].Event.Pressure.ShouldBe(0.0f); + testNode.Events[1].ProcessFrame.ShouldBe(testNode.Events[0].ProcessFrame); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public async Task HoldJoypadButtonFiresInputEvents() + { + var testNode = new JoypadButtonInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Hold controller device 1's X button with 80% pressure for 2 seconds. Note that this doesn't correspond to + // actual functionality of common gamepads. + var button = JoyButton.X; + var deviceID = 1; + var pressure = 0.8f; + var seconds = 0.5f; + var timeTolerance = 0.1f; + await testNode.HoldJoypadButtonFor(seconds, button, deviceID, pressure); + testNode.Events.Count.ShouldBe(2); + testNode.Events[0].Event.Pressed.ShouldBeTrue(); + testNode.Events[0].Event.ButtonIndex.ShouldBe(button); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[0].Event.Pressure.ShouldBe(pressure); + testNode.Events[1].Event.Pressed.ShouldBeFalse(); + testNode.Events[1].Event.ButtonIndex.ShouldBe(button); + testNode.Events[1].Event.Device.ShouldBe(deviceID); + testNode.Events[1].Event.Pressure.ShouldBe(0.0f); + var timeDiff = testNode.Events[1].DateTime - testNode.Events[0].DateTime; + timeDiff.TotalSeconds.ShouldBe(seconds, timeTolerance); + RootNode.RemoveChild(testNode); // Remove immediately since we won't wait a frame for the free + testNode.QueueFree(); + } + + [Test] + public void MoveJoypadAxisFiresInputEvent() + { + var testNode = new JoypadMotionInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Move controller device 1's right-thumbstick x-axis to the -0.3 position (about 1/3 left stick). + var axis = JoyAxis.RightX; + var deviceID = 1; + var position = -0.3f; + testNode.MoveJoypadAxisTo(axis, position, deviceID); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Axis.ShouldBe(axis); + testNode.Events[0].Event.AxisValue.ShouldBe(position); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + RootNode.RemoveChild(testNode); + testNode.QueueFree(); + } + + [Test] + public void ReleaseJoypadAxisFiresInputEvent() + { + var testNode = new JoypadMotionInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Move controller device 1's right-thumbstick x-axis to the rest position. + var axis = JoyAxis.RightX; + var deviceID = 1; + testNode.ReleaseJoypadAxis(axis, deviceID); + testNode.Events.Count.ShouldBe(1); + testNode.Events[0].Event.Axis.ShouldBe(axis); + testNode.Events[0].Event.AxisValue.ShouldBe(0.0f); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + RootNode.RemoveChild(testNode); + testNode.QueueFree(); + } + + [Test] + public async Task HoldJoypadAxisFiresInputEvents() + { + var testNode = new JoypadMotionInputEventTestNode(); + RootNode.AddChild(testNode); + testNode.Events.Count.ShouldBe(0); + // Move controller device 1's right-thumbstick x-axis to the rest position. + var axis = JoyAxis.RightX; + var deviceID = 1; + var position = -0.3f; + var seconds = 0.5f; + var timeTolerance = 0.1f; + await testNode.HoldJoypadAxisFor(seconds, axis, position, deviceID); + testNode.Events.Count.ShouldBe(2); + testNode.Events[0].Event.Axis.ShouldBe(axis); + testNode.Events[0].Event.AxisValue.ShouldBe(position); + testNode.Events[0].Event.Device.ShouldBe(deviceID); + testNode.Events[1].Event.Axis.ShouldBe(axis); + testNode.Events[1].Event.AxisValue.ShouldBe(0.0f); + testNode.Events[1].Event.Device.ShouldBe(deviceID); + var timeDiff = testNode.Events[1].DateTime - testNode.Events[0].DateTime; + timeDiff.TotalSeconds.ShouldBe(seconds, timeTolerance); + RootNode.RemoveChild(testNode); + testNode.QueueFree(); + } +} diff --git a/GodotTestDriver/Input/ActionsInputExtensions.cs b/GodotTestDriver/Input/ActionsInputExtensions.cs index b59027c..22e394c 100644 --- a/GodotTestDriver/Input/ActionsInputExtensions.cs +++ b/GodotTestDriver/Input/ActionsInputExtensions.cs @@ -1,5 +1,5 @@ -namespace Chickensoft.GodotTestDriver.Input; - +namespace Chickensoft.GodotTestDriver.Input; + using System; using System.Threading.Tasks; using Godot;