Wire the Core pieces into a runnable tray app with per-game profiles and the three-state serial-yield auto-switch: - Profiles/: RioProfile + AppConfig model; ConfigStore (System.Text.Json, round-tripped); RioIniImporter ports the legacy RIO.ini (button table, invert flags, plasma greeting); AutoSwitchResolver + AutoSwitchWatcher resolve the foreground executable into Yield (native game) / Activate (profile) / Idle, with native always winning and change-only notifications. IForegroundProcessProvider abstracts the OS. - RioRuntime assembles a profile's live pipeline: serial ButtonPressed/Released + KeyPressed/Released → InputRouter (via RioAddress); AnalogReply → AxisCalibrator → the six joystick axes; RIO commands → calibration resets + version/check requests + lamp re-init. SerialLampSink sends lamp feedback over the link; NullJoystickSink is a placeholder until the Phase 1 HID feeder exists. - RioJoy.Tray: NotifyIcon menu mirroring the legacy console menu (axis resets, version/status, raw-axes & poll-rate toggles, quit) + profile selection (auto vs. manual) + "start with Windows"; RioCoordinator owns the serial acquire/release tied to the watcher (native-game COM-port yield). OS adapters: ForegroundProcessProvider (Win32 foreground PID→exe) and AutoStartManager (HKCU Run key). - tests: 18 new xUnit tests (123 total) for config round-trip, ini import, the three-state resolver + watcher, and RioRuntime end-to-end over the fake transport (button→joystick, keypad-offset→keyboard, analog→six axes). The joystick output stays a no-op until the Phase 1 driver; on-cabinet verification of the acquire/release lifecycle remains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
3.1 KiB
C#
98 lines
3.1 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 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;
|
|
}
|
|
}
|