diff --git a/src/VRio.App/MainForm.cs b/src/VRio.App/MainForm.cs
index 6c210ef..5fd415e 100644
--- a/src/VRio.App/MainForm.cs
+++ b/src/VRio.App/MainForm.cs
@@ -49,24 +49,25 @@ internal sealed class MainForm : Form
private readonly CheckBox _kbInput = new() { Text = "Keyboard", Location = new Point(10, 22), AutoSize = true, Checked = true };
private readonly CheckBox _padInput = new() { Text = "Xbox gamepad", Location = new Point(120, 22), AutoSize = true, Checked = true };
+ private readonly CheckBox _invertY = new() { Text = "Invert joystick Y", Location = new Point(10, 46), AutoSize = true, Checked = true };
private readonly Label _padStatus = new()
{
Text = "No controller detected.",
- Location = new Point(10, 46),
+ Location = new Point(10, 70),
AutoSize = true,
ForeColor = Color.Gray,
};
- private readonly Button _reloadBindings = new() { Text = "Reload bindings", Location = new Point(10, 68), Width = 140, Height = 26 };
- private readonly Button _editBindings = new() { Text = "Edit bindings…", Location = new Point(156, 68), Width = 140, Height = 26 };
+ private readonly Button _reloadBindings = new() { Text = "Reload bindings", Location = new Point(10, 92), Width = 140, Height = 26 };
+ private readonly Button _editBindings = new() { Text = "Edit bindings…", Location = new Point(156, 92), Width = 140, Height = 26 };
private readonly CheckBox _kbLights = new()
{
Text = "Mirror lamps on RGB keyboard (Dynamic Lighting)",
- Location = new Point(10, 100),
+ Location = new Point(10, 124),
AutoSize = true,
};
private readonly ComboBox _kbLightsTarget = new()
{
- Location = new Point(10, 122),
+ Location = new Point(10, 146),
Width = 286,
DropDownStyle = ComboBoxStyle.DropDownList,
Enabled = false, // populated while the mirror is on
@@ -80,14 +81,14 @@ internal sealed class MainForm : Form
private readonly Label _counters = new()
{
- Location = new Point(12, 390),
+ Location = new Point(12, 414),
AutoSize = true,
Font = new Font("Consolas", 8f),
};
private readonly Label _help = new()
{
- Location = new Point(12, 446),
+ Location = new Point(12, 470),
MaximumSize = new Size(306, 0),
AutoSize = true,
ForeColor = Color.Gray,
@@ -98,7 +99,7 @@ internal sealed class MainForm : Form
private readonly TextBox _logBox = new()
{
- Location = new Point(12, 530),
+ Location = new Point(12, 554),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Both, // long wire lines don't wrap — scroll to read
@@ -122,7 +123,8 @@ internal sealed class MainForm : Form
public MainForm()
{
- Text = "vRIO — Virtual RIO cockpit device";
+ // ProductVersion carries the git stamp (see StampGitVersion in the csproj).
+ Text = $"vRIO v{Application.ProductVersion} — Virtual RIO cockpit device";
// Fit the window to its content: the cockpit canvas plus the 330px
// control strip, with just enough height for the strip's log area.
ClientSize = new Size(_canvas.Width + 332, Math.Max(_canvas.Height, 640));
@@ -188,6 +190,8 @@ internal sealed class MainForm : Form
if (!_kbInput.Checked)
_router.ReleaseAllKeys();
};
+ _invertY.CheckedChanged += (_, _) => _router.InvertJoystickY = _invertY.Checked;
+ _router.InvertJoystickY = _invertY.Checked; // the field initializer raises no event
_kbLights.CheckedChanged += (_, _) =>
{
_lampMirror.Enabled = _kbLights.Checked;
@@ -250,13 +254,13 @@ internal sealed class MainForm : Form
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff, _testEnter, _testExit });
panel.Controls.Add(device);
- var input = new GroupBox { Text = "Input", Location = new Point(12, 224), Size = new Size(306, 158) };
- input.Controls.AddRange(new Control[] { _kbInput, _padInput, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget });
+ var input = new GroupBox { Text = "Input", Location = new Point(12, 224), Size = new Size(306, 182) };
+ input.Controls.AddRange(new Control[] { _kbInput, _padInput, _invertY, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget });
panel.Controls.Add(input);
panel.Controls.Add(_counters);
panel.Controls.Add(_help);
- panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 512), AutoSize = true });
+ panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 536), AutoSize = true });
_logBox.Size = new Size(306, ClientSize.Height - _logBox.Top - 44);
panel.Controls.Add(_logBox);
diff --git a/src/VRio.App/VRio.App.csproj b/src/VRio.App/VRio.App.csproj
index e755852..d2e9355 100644
--- a/src/VRio.App/VRio.App.csproj
+++ b/src/VRio.App/VRio.App.csproj
@@ -9,12 +9,37 @@
latest
app.manifest
vRIO — Virtual RIO device
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ $(GitCommitDate.Trim().Replace('-', '.')) ($(GitShortSha))
+
+
+
all
diff --git a/src/VRio.Core/Input/InputRouter.cs b/src/VRio.Core/Input/InputRouter.cs
index fac9776..f00221b 100644
--- a/src/VRio.Core/Input/InputRouter.cs
+++ b/src/VRio.Core/Input/InputRouter.cs
@@ -57,6 +57,13 @@ public sealed class InputRouter
/// A routed address went down (true) or came back up (false) — for panel highlighting.
public event Action? AddressHeldChanged;
+ ///
+ /// Flip the sign of the composed joystick Y before it reaches the device.
+ /// Applies to all routed sources (keys and pad) alike; panel drags write
+ /// the device directly and are unaffected.
+ ///
+ public bool InvertJoystickY { get; set; }
+
///
/// The active bindings. Assigning releases everything currently held
/// (keys, toggles, pad buttons) so a reload never strands a pressed input.
@@ -216,6 +223,8 @@ public sealed class InputRouter
if (b.Rate <= 0f)
total += Shape(_pad.Axis(b.Source), b);
}
+ if (InvertJoystickY && axis == RioAxis.JoystickY)
+ total = -total;
total = ClampNorm(axis, total);
short raw = (short)Math.Round(total * RioAxisRange.Full(axis));
diff --git a/tests/VRio.Core.Tests/InputRouterTests.cs b/tests/VRio.Core.Tests/InputRouterTests.cs
index 2be571e..64d5863 100644
--- a/tests/VRio.Core.Tests/InputRouterTests.cs
+++ b/tests/VRio.Core.Tests/InputRouterTests.cs
@@ -191,6 +191,26 @@ public class InputRouterTests
Assert.Equal(0, _device.GetAxis(RioAxis.JoystickY));
}
+ [Fact]
+ public void InvertJoystickY_flips_the_composed_value_for_all_sources()
+ {
+ UseProfile("key Up axis JoystickY deflect 1\npadaxis LeftStickY axis JoystickY");
+ _router.InvertJoystickY = true;
+
+ _router.KeyDown("Up");
+ _router.Tick(0.016);
+ Assert.Equal(-RioAxisRange.JoystickExtent, _device.GetAxis(RioAxis.JoystickY));
+ _router.KeyUp("Up");
+
+ _router.SetPadState(new PadState(PadButtons.None, 0, 0.5f, 0, 0, 0, 0));
+ _router.Tick(0.016);
+ Assert.Equal(-RioAxisRange.JoystickExtent / 2, _device.GetAxis(RioAxis.JoystickY));
+
+ _router.InvertJoystickY = false; // toggling back re-sends the upright value
+ _router.Tick(0.016);
+ Assert.Equal(RioAxisRange.JoystickExtent / 2, _device.GetAxis(RioAxis.JoystickY));
+ }
+
[Fact]
public void Rate_key_walks_the_throttle_and_position_sticks()
{
diff --git a/tests/VRio.Core.Tests/VRioDeviceTests.cs b/tests/VRio.Core.Tests/VRioDeviceTests.cs
index a98688b..d8ee2da 100644
--- a/tests/VRio.Core.Tests/VRioDeviceTests.cs
+++ b/tests/VRio.Core.Tests/VRioDeviceTests.cs
@@ -48,6 +48,7 @@ public class VRioDeviceTests
var device = new VRioDevice();
var wire = new Wire(device);
device.SetAxis(RioAxis.Throttle, 1000);
+ device.SetAxis(RioAxis.JoystickY, 3000);
device.SetAxis(RioAxis.JoystickX, -5000);
Send(device, PacketBuilder.Build(RioCommand.AnalogRequest));
@@ -60,7 +61,7 @@ public class VRioDeviceTests
Assert.Equal(1000, AnalogCodec.Combine(p[0], p[1])); // throttle
Assert.Equal(0, AnalogCodec.Combine(p[2], p[3])); // left pedal
Assert.Equal(0, AnalogCodec.Combine(p[4], p[5])); // right pedal
- Assert.Equal(0, AnalogCodec.Combine(p[6], p[7])); // joystick Y
+ Assert.Equal(3000, AnalogCodec.Combine(p[6], p[7])); // joystick Y
Assert.Equal(-5000, AnalogCodec.Combine(p[8], p[9])); // joystick X
Assert.Equal(1, device.AnalogRequests);
}