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;