using RioJoy.Core.Calibration; using RioJoy.Core.Output; using Xunit; namespace RioJoy.Core.Tests.Output; public class AxisRouterTests { // --- Default routing reproduces the legacy hardcoded sink exactly -------- [Theory] [InlineData(JoyAxis.X, PadTarget.LeftThumbX)] [InlineData(JoyAxis.Y, PadTarget.LeftThumbY)] [InlineData(JoyAxis.Rx, PadTarget.RightThumbX)] [InlineData(JoyAxis.Ry, PadTarget.RightThumbY)] public void Default_ThumbAxes_UseLegacyCenteredMath(JoyAxis axis, PadTarget expected) { var config = new AxisRoutingConfig(); // Legacy ToThumb: (value - 16383) * 2, clamped to the short range. ResolvedAxis min = AxisRouter.Resolve(config, axis, 0); Assert.Equal(expected, min.Target); Assert.Equal(-32766, min.Value); Assert.Equal(0, AxisRouter.Resolve(config, axis, AxisOutputs.Center).Value); Assert.Equal(32766, AxisRouter.Resolve(config, axis, AxisOutputs.Max).Value); } [Theory] [InlineData(JoyAxis.Z, PadTarget.LeftTrigger)] [InlineData(JoyAxis.Rz, PadTarget.RightTrigger)] public void Default_TriggerAxes_UseLegacyTriggerMath(JoyAxis axis, PadTarget expected) { var config = new AxisRoutingConfig(); // Legacy ToTrigger: value * 255 / 32766, clamped to the byte range. ResolvedAxis rest = AxisRouter.Resolve(config, axis, 0); Assert.Equal(expected, rest.Target); Assert.Equal(0, rest.Value); Assert.Equal(127, AxisRouter.Resolve(config, axis, AxisOutputs.Center).Value); Assert.Equal(255, AxisRouter.Resolve(config, axis, AxisOutputs.Max).Value); } [Fact] public void Default_CenteredThumb_ClampsToShortRange() { var config = new AxisRoutingConfig(); // (40000 - 16383) * 2 = 47234 → clamped to short.MaxValue. Assert.Equal(32767, AxisRouter.Resolve(config, JoyAxis.X, 40000).Value); // (-2000 - 16383) * 2 = -36766 → clamped to short.MinValue. Assert.Equal(-32768, AxisRouter.Resolve(config, JoyAxis.X, -2000).Value); } // --- UnipolarPositive: clamp(value, 0, 32767) ---------------------------- [Fact] public void UnipolarPositive_MapsCalibratedZeroToThumbCenter() { var config = new AxisRoutingConfig { Z = new AxisRoute { Target = PadTarget.RightThumbY, Mode = AxisOutputMode.UnipolarPositive }, }; Assert.Equal(0, AxisRouter.Resolve(config, JoyAxis.Z, 0).Value); // detent → center Assert.Equal(16383, AxisRouter.Resolve(config, JoyAxis.Z, 16383).Value); // half → half up Assert.Equal(32766, AxisRouter.Resolve(config, JoyAxis.Z, AxisOutputs.Max).Value); // full → max (32766 of 32767) Assert.Equal(0, AxisRouter.Resolve(config, JoyAxis.Z, -5).Value); // clamps below Assert.Equal(32767, AxisRouter.Resolve(config, JoyAxis.Z, 40000).Value); // clamps above } [Fact] public void UnipolarPositive_OnTriggerTarget_IsIgnored_TriggerMathApplies() { // Mode is meaningless for trigger targets — they keep the trigger byte math. var config = new AxisRoutingConfig { Z = new AxisRoute { Target = PadTarget.LeftTrigger, Mode = AxisOutputMode.UnipolarPositive }, }; Assert.Equal(255, AxisRouter.Resolve(config, JoyAxis.Z, AxisOutputs.Max).Value); } // --- None: the axis is not emitted --------------------------------------- [Fact] public void NoneTarget_EmitsNothing() { var config = new AxisRoutingConfig { Rx = new AxisRoute { Target = PadTarget.None }, }; ResolvedAxis r = AxisRouter.Resolve(config, JoyAxis.Rx, 12345); Assert.Equal(PadTarget.None, r.Target); Assert.Equal(0, r.Value); } // --- The Descent shape ---------------------------------------------------- [Fact] public void DescentConfig_ThrottleToRightThumbY_TriggersUntargeted() { // profiles/descent-d1x.json: DXX's stock GameController defaults fire on // the LT/RT axis-buttons, so no axis may land on a trigger. var config = new AxisRoutingConfig { Z = new AxisRoute { Target = PadTarget.RightThumbY, Mode = AxisOutputMode.UnipolarPositive }, Rz = new AxisRoute { Target = PadTarget.RightThumbX }, Rx = new AxisRoute { Target = PadTarget.None }, Ry = new AxisRoute { Target = PadTarget.None }, }; ResolvedAxis z = AxisRouter.Resolve(config, JoyAxis.Z, 16383); Assert.Equal(PadTarget.RightThumbY, z.Target); Assert.Equal(16383, z.Value); // unipolar: half throttle → half deflection up ResolvedAxis rz = AxisRouter.Resolve(config, JoyAxis.Rz, AxisOutputs.Center); Assert.Equal(PadTarget.RightThumbX, rz.Target); Assert.Equal(0, rz.Value); // rudder stays bipolar: center → thumb 0 foreach (JoyAxis axis in new[] { JoyAxis.X, JoyAxis.Y, JoyAxis.Z, JoyAxis.Rx, JoyAxis.Ry, JoyAxis.Rz }) { PadTarget target = AxisRouter.Resolve(config, axis, AxisOutputs.Center).Target; Assert.NotEqual(PadTarget.LeftTrigger, target); Assert.NotEqual(PadTarget.RightTrigger, target); } } }