RioProfile gains a nullable AxisRouting section mapping each calibrated axis (X/Y/Z/Rx/Ry/Rz) to a pad target (thumbs, triggers, or None) with a Centered or UnipolarPositive conversion; the default reproduces the old hardcoded routing exactly, so existing profiles are untouched. Routing resolution lives in a pure, ViGEm-free AxisRouter for testability; the sink neutralizes the pad on routing change so stale trigger state cannot leak across profile switches. Motivation: Descent reads the pad via SDL GameController, where the triggers are its stock fire axis-buttons - the old fixed routing put throttle on LeftTrigger (fires) and detent would have read as full reverse. descent-d1x.json now routes Z->RightThumbY (UnipolarPositive, detent = center) and Rz->RightThumbX, triggers untargeted; guarded by tests that parse the shipped JSON through the real deserializer and byte-compare the dxx-rebirth reference copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
68 lines
2.5 KiB
C#
68 lines
2.5 KiB
C#
using RioJoy.Core.Calibration;
|
|
|
|
namespace RioJoy.Core.Output;
|
|
|
|
/// <summary>
|
|
/// A resolved axis write: which pad control to drive and the converted value.
|
|
/// <see cref="Value"/> is in the thumb range (<c>-32768..32767</c>) for thumb
|
|
/// targets, the trigger range (<c>0..255</c>) for trigger targets, and 0 for
|
|
/// <see cref="PadTarget.None"/> (nothing is written).
|
|
/// </summary>
|
|
public readonly struct ResolvedAxis
|
|
{
|
|
public PadTarget Target { get; }
|
|
|
|
public int Value { get; }
|
|
|
|
public ResolvedAxis(PadTarget target, int value)
|
|
{
|
|
Target = target;
|
|
Value = value;
|
|
}
|
|
|
|
public override string ToString() => $"{Target}:{Value}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pure route-resolution + value-conversion for the ViGEm sink — no ViGEm types,
|
|
/// so the decision logic is unit-testable without the bus driver.
|
|
/// <see cref="ViGEmJoystickSink"/> translates the result into ViGEm calls.
|
|
/// </summary>
|
|
public static class AxisRouter
|
|
{
|
|
/// <summary>
|
|
/// Resolve where the calibrated <paramref name="value"/> (<c>0..32766</c>,
|
|
/// center 16383) of <paramref name="axis"/> lands under
|
|
/// <paramref name="config"/>, and convert it for that target.
|
|
/// </summary>
|
|
public static ResolvedAxis Resolve(AxisRoutingConfig config, JoyAxis axis, int value)
|
|
{
|
|
if (config is null) throw new ArgumentNullException(nameof(config));
|
|
|
|
AxisRoute route = config.RouteFor(axis);
|
|
return route.Target switch
|
|
{
|
|
PadTarget.None => new ResolvedAxis(PadTarget.None, 0),
|
|
PadTarget.LeftTrigger or PadTarget.RightTrigger =>
|
|
new ResolvedAxis(route.Target, ToTrigger(value)),
|
|
_ => new ResolvedAxis(
|
|
route.Target,
|
|
route.Mode == AxisOutputMode.UnipolarPositive
|
|
? ToUnipolarThumb(value)
|
|
: ToCenteredThumb(value)),
|
|
};
|
|
}
|
|
|
|
// RIO axis 0..32766 (centre 16383) -> Xbox thumb short -32768..32767 (legacy math).
|
|
private static int ToCenteredThumb(int value) =>
|
|
Compat.Net48Math.Clamp((value - AxisOutputs.Center) * 2, (int)short.MinValue, short.MaxValue);
|
|
|
|
// Unipolar RIO axis (rest = 0) -> upper thumb half: 0 -> center 0, 32766 -> max.
|
|
private static int ToUnipolarThumb(int value) =>
|
|
Compat.Net48Math.Clamp(value, 0, (int)short.MaxValue);
|
|
|
|
// RIO axis 0..32766 -> Xbox trigger byte 0..255 (legacy math).
|
|
private static int ToTrigger(int value) =>
|
|
Compat.Net48Math.Clamp(value * 255 / AxisOutputs.Max, 0, 255);
|
|
}
|