using VRio.Core.Device; using VRio.Core.Input; using VRio.Core.Protocol; using Xunit; namespace VRio.Core.Tests; public class BindingProfileFormatTests { [Fact] public void Default_profile_parses_clean() { BindingProfile profile = BindingProfileFormat.Parse(BindingProfileFormat.DefaultText, out var errors); Assert.Empty(errors); Assert.Equal(73, profile.KeyButtons.Count); // 40 MFD + 12 columns + 16 keypad + Space + 4 hat Assert.Empty(profile.KeyAxes); // axes are pad-only in the default profile Assert.Equal(10, profile.PadButtons.Count); Assert.Equal(5, profile.PadAxes.Count); } [Fact] public void Lines_parse_into_typed_bindings() { BindingProfile profile = BindingProfileFormat.Parse( "key SPACE button 0x40 toggle # comment\n" + "key w axis throttle rate -0.5\n" + "pad dpadup button 81\n" + "padaxis lefttrigger axis LeftPedal invert deadzone 0.25 rate 2\n", out var errors); Assert.Empty(errors); KeyButtonBinding kb = Assert.Single(profile.KeyButtons); Assert.Equal(("SPACE", 0x40, true), (kb.Key, kb.Address, kb.Toggle)); KeyAxisBinding ka = Assert.Single(profile.KeyAxes); Assert.Equal((RioAxis.Throttle, KeyAxisMode.Rate, -0.5f), (ka.Axis, ka.Mode, ka.Value)); PadButtonBinding pb = Assert.Single(profile.PadButtons); Assert.Equal((PadButtons.DPadUp, 81), (pb.Button, pb.Address)); // decimal keypad address PadAxisBinding pa = Assert.Single(profile.PadAxes); Assert.Equal((PadAxis.LeftTrigger, RioAxis.LeftPedal, true, 0.25f, 2f), (pa.Source, pa.Axis, pa.Invert, pa.Deadzone, pa.Rate)); } [Theory] [InlineData("key W button 0x48")] // hole between buttons and keypads [InlineData("key W button zz")] [InlineData("key W axis Throttle deflect")] // missing value [InlineData("key W axis Throttle wiggle 1")] // unknown mode [InlineData("pad Guide button 0x00")] // unknown pad button [InlineData("padaxis LeftStickX axis JoystickX sideways")] [InlineData("mouse X button 0x00")] // unknown source public void Bad_lines_are_reported_and_skipped(string line) { BindingProfile profile = BindingProfileFormat.Parse("key A button 0x00\n" + line, out var errors); string error = Assert.Single(errors); Assert.StartsWith("line 2:", error); Assert.Equal(1, profile.Count); // the good line survived } } public class InputRouterTests { private readonly VRioDevice _device = new(); private readonly InputRouter _router; private readonly List _tx = new(); public InputRouterTests() { _router = new InputRouter(_device); _device.Transmit += _tx.Add; } private void UseProfile(string text) { _router.Profile = BindingProfileFormat.Parse(text, out var errors); Assert.Empty(errors); _tx.Clear(); } // ---- Buttons ------------------------------------------------------------ [Fact] public void Key_press_and_release_hit_the_wire_once_despite_repeats() { UseProfile("key Space button 0x40"); _router.KeyDown("Space"); _router.KeyDown("Space"); // auto-repeat _router.KeyDown("Space"); _router.KeyUp("Space"); Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x40), PacketBuilder.ButtonReleased(0x40) }, _tx); } [Fact] public void Keypad_addresses_go_out_as_key_events() { UseProfile("key NumPad7 button 0x67"); // external keypad, key 7 _router.KeyDown("NumPad7"); _router.KeyUp("NumPad7"); Assert.Equal(new[] { PacketBuilder.KeyPressed(1, 7), PacketBuilder.KeyReleased(1, 7) }, _tx); } [Fact] public void Toggle_latches_across_key_up() { UseProfile("key T button 0x12 toggle"); _router.KeyDown("T"); _router.KeyUp("T"); Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x12) }, _tx); _router.KeyDown("T"); // second press releases the latch _router.KeyUp("T"); Assert.Equal(PacketBuilder.ButtonReleased(0x12), _tx[1]); } [Fact] public void Overlapping_sources_on_one_address_press_once_release_last() { UseProfile("key Space button 0x40\npad A button 0x40"); var held = new List<(int Address, bool Held)>(); _router.AddressHeldChanged += (a, h) => held.Add((a, h)); _router.KeyDown("Space"); _router.SetPadState(new PadState(PadButtons.A, 0, 0, 0, 0, 0, 0)); _router.KeyUp("Space"); Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x40) }, _tx); // still held by the pad _router.SetPadState(default); Assert.Equal(PacketBuilder.ButtonReleased(0x40), _tx[1]); Assert.Equal(new[] { (0x40, true), (0x40, false) }, held); } [Fact] public void Pad_button_edges_only_fire_on_change() { UseProfile("pad B button 0x3D"); var down = new PadState(PadButtons.B, 0, 0, 0, 0, 0, 0); _router.SetPadState(down); _router.SetPadState(down); // unchanged snapshot _router.SetPadState(default); Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x3D), PacketBuilder.ButtonReleased(0x3D) }, _tx); } [Fact] public void ReleaseAllKeys_releases_momentaries_but_keeps_toggles() { UseProfile("key A button 0x01\nkey B button 0x02 toggle"); _router.KeyDown("A"); _router.KeyDown("B"); _router.ReleaseAllKeys(); Assert.Equal(0, _device.GetLamp(0)); // sanity: device untouched otherwise Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x01), PacketBuilder.ButtonPressed(0x02), PacketBuilder.ButtonReleased(0x01), // toggle at 0x02 stays latched }, _tx); } // ---- Axes --------------------------------------------------------------- [Fact] public void Deflect_key_holds_full_travel_and_springs_back() { UseProfile("key Up axis JoystickY deflect 1\nkey Down axis JoystickY deflect -1"); _router.KeyDown("Up"); _router.Tick(0.016); Assert.Equal(RioAxisRange.JoystickExtent, _device.GetAxis(RioAxis.JoystickY)); _router.KeyDown("Down"); // opposite deflects cancel _router.Tick(0.016); Assert.Equal(0, _device.GetAxis(RioAxis.JoystickY)); _router.KeyUp("Up"); _router.KeyUp("Down"); _router.Tick(0.016); Assert.Equal(0, _device.GetAxis(RioAxis.JoystickY)); } [Fact] public void Rate_key_walks_the_throttle_and_position_sticks() { UseProfile("key W axis Throttle rate 0.5"); _router.KeyDown("W"); _router.Tick(1.0); // half travel Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle)); _router.KeyUp("W"); _router.Tick(1.0); // released: stays put Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle)); _router.KeyDown("W"); _router.Tick(10.0); // clamps at full realistic travel Assert.Equal(RioAxisRange.ThrottleFull, _device.GetAxis(RioAxis.Throttle)); } [Fact] public void Pad_stick_maps_through_deadzone_and_invert() { UseProfile("padaxis LeftStickX axis JoystickX invert deadzone 0.2"); // Inside the deadzone: centered. _router.SetPadState(new PadState(PadButtons.None, 0.1f, 0, 0, 0, 0, 0)); _router.Tick(0.016); Assert.Equal(0, _device.GetAxis(RioAxis.JoystickX)); // Full right, inverted → raw negative full extent (the wire direction). _router.SetPadState(new PadState(PadButtons.None, 1f, 0, 0, 0, 0, 0)); _router.Tick(0.016); Assert.Equal(-RioAxisRange.JoystickExtent, _device.GetAxis(RioAxis.JoystickX)); // Pad gone → springs back. _router.SetPadState(default); _router.Tick(0.016); Assert.Equal(0, _device.GetAxis(RioAxis.JoystickX)); } [Fact] public void Pad_trigger_drives_pedal_to_full_press() { UseProfile("padaxis RightTrigger axis RightPedal"); _router.SetPadState(new PadState(PadButtons.None, 0, 0, 0, 0, 0, 1f)); _router.Tick(0.016); Assert.Equal(RioAxisRange.PedalFull, _device.GetAxis(RioAxis.RightPedal)); } [Fact] public void Rate_mode_pad_axis_integrates_and_holds() { UseProfile("padaxis RightStickY axis Throttle rate 0.5"); _router.SetPadState(new PadState(PadButtons.None, 0, 0, 0, 1f, 0, 0)); _router.Tick(1.0); Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle)); _router.SetPadState(default); // stick released: throttle stays _router.Tick(1.0); Assert.Equal(RioAxisRange.ThrottleFull / 2, _device.GetAxis(RioAxis.Throttle)); } [Fact] public void ResetAxisState_forgets_integrated_position() { UseProfile("key W axis Throttle rate 1"); _router.KeyDown("W"); _router.Tick(0.5); _router.KeyUp("W"); Assert.NotEqual(0, _device.GetAxis(RioAxis.Throttle)); // Emulate the center button / host reset combination. _device.SetAxis(RioAxis.Throttle, 0); _router.ResetAxisState(); _router.Tick(0.016); Assert.Equal(0, _device.GetAxis(RioAxis.Throttle)); } [Fact] public void Idle_router_does_not_fight_other_axis_writers() { UseProfile("padaxis LeftStickX axis JoystickX"); _router.Tick(0.016); _device.SetAxis(RioAxis.JoystickX, 42); // e.g. a mouse drag on the panel _router.Tick(0.016); // router value unchanged → no clobber Assert.Equal(42, _device.GetAxis(RioAxis.JoystickX)); } [Fact] public void Profile_swap_releases_held_inputs() { UseProfile("key A button 0x05"); _router.KeyDown("A"); _router.Profile = BindingProfile.Empty; Assert.Equal(new[] { PacketBuilder.ButtonPressed(0x05), PacketBuilder.ButtonReleased(0x05) }, _tx); } }