Profile editor: - Cockpit-style encoder gauges centered over the Upper Middle MFD: Z left, X/Y right, and L/R/Rz arranged as a big "U". - [JoyStick] section surfaced as a vertical "Axis" toggle column (6 inverts + ZR mix), written back to the profile's calibration. - Tighten the panel layout (close gaps, trim empty border columns); colour the Secondary/Screen columns yellow; rename the "Joystick / Hat" board to "Joystick"; add an "Import .ini..." tray entry. - "RIO commands (live)" button group (all RioCommandCodes) fired at the live RIO. Serial-port handling: - Start dormant; only take the COM port for a profiled game (auto-switch) or while editing, so the native games can open the port without clashing. - Editor sessions hold the port but route keyboard/mouse/joystick to no-op sinks (NullInputSink), so button function can be checked without injecting input; RioRuntime.ButtonActivity drives a live cyan press indicator and lamp feedback still applies. - AutoSwitchWatcher.Reset() re-syncs the watcher after an editor session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
4.2 KiB
C#
126 lines
4.2 KiB
C#
using System.Diagnostics;
|
|
using RioJoy.Core;
|
|
using RioJoy.Core.Mapping;
|
|
using RioJoy.Core.Protocol;
|
|
using RioJoy.Core.Serial;
|
|
using RioJoy.Core.Tests.Mapping;
|
|
using RioJoy.Core.Tests.Serial;
|
|
using Xunit;
|
|
|
|
namespace RioJoy.Core.Tests;
|
|
|
|
public class RioRuntimeTests
|
|
{
|
|
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
|
|
|
|
private static async Task WaitUntilAsync(Func<bool> condition)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
while (!condition())
|
|
{
|
|
if (sw.Elapsed > Timeout)
|
|
throw new TimeoutException("Condition not met in time.");
|
|
await Task.Delay(10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ButtonPressed_RoutesToJoystick()
|
|
{
|
|
var fake = new FakeTransport();
|
|
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
|
|
var recorder = new RecordingSink();
|
|
|
|
var map = new RioInputMap { [0x05] = new RioMapEntry(0x1009) }; // joystick button 9
|
|
using var runtime = new RioRuntime(link, map, recorder, recorder);
|
|
runtime.Start();
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
Task run = link.RunAsync(cts.Token);
|
|
|
|
fake.Enqueue(PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 }));
|
|
|
|
await WaitUntilAsync(() => recorder.Snapshot().Contains("Joy(9,True)"));
|
|
|
|
cts.Cancel();
|
|
await run;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnalogReply_DrivesAllSixAxes()
|
|
{
|
|
var fake = new FakeTransport();
|
|
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
|
|
var recorder = new RecordingSink();
|
|
|
|
using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder);
|
|
runtime.Start();
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
Task run = link.RunAsync(cts.Token);
|
|
|
|
// All axes at zero → joystick X/Y centered.
|
|
fake.Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, new byte[10]));
|
|
|
|
await WaitUntilAsync(() => recorder.Snapshot().Count(e => e.StartsWith("Axis(")) == 6);
|
|
|
|
string[] log = recorder.Snapshot();
|
|
Assert.Contains("Axis(X,16383)", log);
|
|
Assert.Contains("Axis(Y,16383)", log);
|
|
|
|
cts.Cancel();
|
|
await run;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ButtonActivity_FiresForPressAndRelease_EvenWhenUnmapped()
|
|
{
|
|
var fake = new FakeTransport();
|
|
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
|
|
var recorder = new RecordingSink();
|
|
|
|
using var runtime = new RioRuntime(link, new RioInputMap(), recorder, recorder); // empty map
|
|
var activity = new List<(int Address, bool Pressed)>();
|
|
runtime.ButtonActivity += (addr, pressed) => activity.Add((addr, pressed));
|
|
runtime.Start();
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
Task run = link.RunAsync(cts.Token);
|
|
|
|
fake.Enqueue(PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 }));
|
|
fake.Enqueue(PacketBuilder.Build(RioCommand.ButtonReleased, new byte[] { 0x05 }));
|
|
|
|
await WaitUntilAsync(() => activity.Count == 2);
|
|
|
|
Assert.Equal((0x05, true), activity[0]);
|
|
Assert.Equal((0x05, false), activity[1]);
|
|
Assert.Empty(recorder.Snapshot()); // unmapped → no output routed, but activity still fired
|
|
|
|
cts.Cancel();
|
|
await run;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task KeypadKey_RoutesThroughOffsetAddress()
|
|
{
|
|
var fake = new FakeTransport();
|
|
var link = new RioSerialLink(fake, new RioSerialLinkOptions { AutoPollAnalog = false });
|
|
var recorder = new RecordingSink();
|
|
|
|
// Keypad 1, index 2 → address 0x62; map a keyboard key there (VK 0x41 = 'A').
|
|
var map = new RioInputMap { [0x62] = new RioMapEntry(0x0041) };
|
|
using var runtime = new RioRuntime(link, map, recorder, recorder);
|
|
runtime.Start();
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
Task run = link.RunAsync(cts.Token);
|
|
|
|
fake.Enqueue(PacketBuilder.Build(RioCommand.KeyPressed, new byte[] { 1, 2 }));
|
|
|
|
await WaitUntilAsync(() => recorder.Snapshot().Contains("KeyDown(0x41,ext=False)"));
|
|
|
|
cts.Cancel();
|
|
await run;
|
|
}
|
|
}
|