Files
riojoy/src/RioJoy.Core/Output/AxisRoutingConfig.cs
T
CydandClaude Fable 5 2f2438717a core: per-profile ViGEm axis routing with unipolar output mode
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>
2026-07-30 09:00:09 -05:00

83 lines
2.9 KiB
C#

using RioJoy.Core.Calibration;
namespace RioJoy.Core.Output;
/// <summary>
/// Where a calibrated axis lands on the virtual Xbox 360 pad. Our own enum (no
/// ViGEm types — these are shared config types, serialized into profiles and
/// compiled for net40 too); <see cref="ViGEmJoystickSink"/> translates to the
/// ViGEm equivalents. <see cref="None"/> = the axis is not emitted at all.
/// </summary>
public enum PadTarget
{
LeftThumbX,
LeftThumbY,
RightThumbX,
RightThumbY,
LeftTrigger,
RightTrigger,
None,
}
/// <summary>
/// How a calibrated axis value (<c>0..32766</c>, center 16383) converts to a
/// thumb-stick value. Meaningless for trigger targets (triggers always use the
/// legacy <c>value*255/32766</c> byte conversion).
/// </summary>
public enum AxisOutputMode
{
/// <summary>Legacy bipolar mapping: <c>(value - 16383) * 2</c> → center 16383 = thumb 0.</summary>
Centered,
/// <summary>
/// Unipolar mapping for axes whose calibrated rest is 0 (the ratcheted
/// throttle): <c>clamp(value, 0, 32767)</c> → calibrated 0 = thumb center 0,
/// 32766 = thumb max. Only the upper half of the thumb range is used.
/// </summary>
UnipolarPositive,
}
/// <summary>One axis route: which pad control it drives, and how the value converts.</summary>
public sealed record AxisRoute
{
public PadTarget Target { get; init; } = PadTarget.None;
public AxisOutputMode Mode { get; init; } = AxisOutputMode.Centered;
}
/// <summary>
/// Per-profile routing of the six calibrated axes onto the ViGEm Xbox 360 pad
/// (<see cref="RioJoy.Core.Profiles.RioProfile.AxisRouting"/>; null there = this
/// default). The defaults reproduce the historical hardcoded sink routing
/// exactly — X→LeftThumbX, Y→LeftThumbY, Rx→RightThumbX, Ry→RightThumbY,
/// Z→LeftTrigger, Rz→RightTrigger, all <see cref="AxisOutputMode.Centered"/> —
/// so existing profiles behave identically. Two axes routed to the same target
/// are not arbitrated: the last <c>SetAxis</c> write wins.
/// </summary>
public sealed record AxisRoutingConfig
{
public AxisRoute X { get; init; } = new() { Target = PadTarget.LeftThumbX };
public AxisRoute Y { get; init; } = new() { Target = PadTarget.LeftThumbY };
public AxisRoute Z { get; init; } = new() { Target = PadTarget.LeftTrigger };
public AxisRoute Rx { get; init; } = new() { Target = PadTarget.RightThumbX };
public AxisRoute Ry { get; init; } = new() { Target = PadTarget.RightThumbY };
public AxisRoute Rz { get; init; } = new() { Target = PadTarget.RightTrigger };
/// <summary>The route for <paramref name="axis"/>.</summary>
public AxisRoute RouteFor(JoyAxis axis) => axis switch
{
JoyAxis.X => X,
JoyAxis.Y => Y,
JoyAxis.Z => Z,
JoyAxis.Rx => Rx,
JoyAxis.Ry => Ry,
JoyAxis.Rz => Rz,
_ => new AxisRoute(), // unknown axis → None (not emitted)
};
}