Files
riojoy/tests/RioJoy.Core.Tests/Feedback/RumbleLampAdapterTests.cs
T
CydandClaude Fable 5 ad7ac19ab2 feedback: game-to-cockpit endpoint (pipe/UDP lamps+plasma), rumble lamp flash
Phase 9: FeedbackPipeServer (\\.\pipe\riojoy-feedback) + loopback UDP share a
forgiving text line protocol into FeedbackRouter; CoalescingLampScheduler rate-
governs the 9600-baud link; plasma finally wired into activation (greeting,
teardown blank, PlasmaDisplay write lock); ViGEm FeedbackReceived drives
RumbleLampAdapter. Per-profile Feedback config, docs/FEEDBACK.md, 425 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 19:31:03 -05:00

109 lines
3.8 KiB
C#

using RioJoy.Core.Feedback;
using RioJoy.Core.Protocol;
using RioJoy.Core.Tests.Mapping;
using Xunit;
namespace RioJoy.Core.Tests.Feedback;
public class RumbleLampAdapterTests
{
// Flash + Bright/Bright state bytes the bands resolve to.
private const byte SlowBright = 0x3D;
private const byte MedBright = 0x3E;
private const byte FastBright = 0x3F;
[Theory]
[InlineData(0, 24, 0x00)] // below threshold → off
[InlineData(23, 24, 0x00)]
[InlineData(24, 24, SlowBright)] // first third of the remaining range
[InlineData(100, 24, SlowBright)]
[InlineData(101, 24, MedBright)] // second third
[InlineData(177, 24, MedBright)]
[InlineData(178, 24, FastBright)] // top third
[InlineData(255, 24, FastBright)]
[InlineData(0, 0, SlowBright)] // threshold 0 = never off
public void MapMotor_BandsResolveToDocumentedStates(int value, int threshold, byte expected)
{
Assert.Equal(expected, RumbleLampAdapter.MapMotor((byte)value, (byte)threshold));
}
[Fact]
public void MapMotor_MatchesRioLampStateCompose()
{
Assert.Equal(
RioLampState.Compose(LampFlash.FlashFast, LampField1.Bright, LampField2.Bright),
RumbleLampAdapter.MapMotor(255, 24));
Assert.Equal(RioLampState.SolidOff, RumbleLampAdapter.MapMotor(0, 24));
}
[Fact]
public async Task OnRumble_DrivesEachMotorsConfiguredAddresses()
{
var sink = new RecordingSink();
var scheduler = new CoalescingLampScheduler(sink, TimeSpan.FromMilliseconds(1));
using var cts = new CancellationTokenSource();
Task pump = scheduler.RunAsync(cts.Token);
var adapter = new RumbleLampAdapter(new RumbleLampConfig
{
LargeMotorLamps = { 0x20, 0x21 },
SmallMotorLamps = { 0x30 },
}, scheduler);
adapter.OnRumble(255, 0); // big hit, no small motor
await FeedbackWait.For(() => sink.Snapshot().Length >= 3);
cts.Cancel();
await pump;
string[] sent = sink.Snapshot();
Assert.Contains("Lamp(0x20,0x3F)", sent); // large motor lamps flash fast
Assert.Contains("Lamp(0x21,0x3F)", sent);
Assert.Contains("Lamp(0x30,0x00)", sent); // small motor lamps confirmed off
}
[Fact]
public async Task OnRumble_RepeatedIdenticalValues_PostNothingNew()
{
var sink = new RecordingSink();
var scheduler = new CoalescingLampScheduler(sink, TimeSpan.FromMilliseconds(1));
using var cts = new CancellationTokenSource();
Task pump = scheduler.RunAsync(cts.Token);
var adapter = new RumbleLampAdapter(
new RumbleLampConfig { LargeMotorLamps = { 0x20 }, SmallMotorLamps = { 0x30 } },
scheduler);
// XInput-style spam: same vibration reported over and over.
for (int i = 0; i < 200; i++)
adapter.OnRumble(200, 0);
await FeedbackWait.For(() => sink.Snapshot().Length >= 2);
await Task.Delay(50);
cts.Cancel();
await pump;
Assert.Equal(2, sink.Snapshot().Length); // one state per motor, ever
}
[Fact]
public async Task OnRumble_ZeroAfterRumble_TurnsTheLampsOff()
{
var sink = new RecordingSink();
var scheduler = new CoalescingLampScheduler(sink, TimeSpan.FromMilliseconds(1));
using var cts = new CancellationTokenSource();
Task pump = scheduler.RunAsync(cts.Token);
var adapter = new RumbleLampAdapter(
new RumbleLampConfig { LargeMotorLamps = { 0x20 } }, scheduler);
adapter.OnRumble(255, 0);
await FeedbackWait.For(() => sink.Snapshot().Contains("Lamp(0x20,0x3F)"));
adapter.OnRumble(0, 0);
await FeedbackWait.For(() => sink.Snapshot().Contains("Lamp(0x20,0x00)"));
cts.Cancel();
await pump;
}
}