-
Notifications
You must be signed in to change notification settings - Fork 6
Engine Library
The PadForge.Engine assembly is a shared class library containing data types, interfaces, and enums used by both the Engine (input pipeline) and App (UI/ViewModel) assemblies. It has no UI dependencies and targets net10.0-windows.
Project file: PadForge.Engine/PadForge.Engine.csproj
Namespace: PadForge.Engine
File: PadForge.Engine/Common/GamepadTypes.cs
Minimal struct matching the XInput XINPUT_GAMEPAD layout. Used as the output of the mapping pipeline (Step 3 -> Step 4 -> Step 5).
public struct Gamepad
{
public ushort Buttons;
public byte LeftTrigger;
public byte RightTrigger;
public short ThumbLX;
public short ThumbLY;
public short ThumbRX;
public short ThumbRY;
public bool IsButtonPressed(ushort flag);
public void SetButton(ushort flag, bool pressed);
public void Clear();
}| Constant | Value | Description |
|---|---|---|
DPAD_UP |
0x0001 |
D-pad up |
DPAD_DOWN |
0x0002 |
D-pad down |
DPAD_LEFT |
0x0004 |
D-pad left |
DPAD_RIGHT |
0x0008 |
D-pad right |
START |
0x0010 |
Start button |
BACK |
0x0020 |
Back button |
LEFT_THUMB |
0x0040 |
Left stick click |
RIGHT_THUMB |
0x0080 |
Right stick click |
LEFT_SHOULDER |
0x0100 |
Left bumper |
RIGHT_SHOULDER |
0x0200 |
Right bumper |
GUIDE |
0x0400 |
Guide/home button |
A |
0x1000 |
A button |
B |
0x2000 |
B button |
X |
0x4000 |
X button |
Y |
0x8000 |
Y button |
File: PadForge.Engine/Common/GamepadTypes.cs
public struct XInputState
{
public uint PacketNumber;
public Gamepad Gamepad;
}Matches the XINPUT_STATE struct layout (packet number + Gamepad).
File: PadForge.Engine/Common/GamepadTypes.cs
Raw vJoy output state for custom (non-gamepad) configurations. Bypasses the fixed Gamepad struct to support arbitrary axis/button/POV counts.
public struct VJoyRawState
{
public short[] Axes; // Up to 8 axes (signed short range -32768..32767)
public uint[] Buttons; // Button state as 4 x 32-bit words = 128 buttons max
public int[] Povs; // Up to 4 POV hat switches (-1=centered, 0-35900=direction)
public static VJoyRawState Create(int nAxes, int nButtons, int nPovs);
public void SetButton(int index, bool pressed);
public bool IsButtonPressed(int index);
public void Clear();
}public static VJoyRawState Create(int nAxes, int nButtons, int nPovs)Creates a zeroed state with specified capacities. Axes clamped to max 8, buttons to max 128 (stored as (N+31)/32 uint words), POVs to max 4.
Buttons use a 128-bit bitmask stored as uint[4]. Each uint holds 32 buttons. Index calculation:
word = index / 32bit = index % 32
POV values are in hundredths of degrees:
-
0= North -
4500= NE -
9000= East -
13500= SE -
18000= South -
22500= SW -
27000= West -
31500= NW -
0xFFFFFFFF(-1) = Centered
Resets axes to 0, buttons to 0, POVs to -1 (centered).
File: PadForge.Engine/Common/GamepadTypes.cs
Dynamic-sized MIDI output state for MidiVirtualController. Bypasses the fixed Gamepad struct to support arbitrary CC and note counts.
public struct MidiRawState
{
public byte[] CcValues; // CC values 0-127 per CC slot
public bool[] Notes; // Note on/off per note slot
public static MidiRawState Create(int ccCount, int noteCount);
}Create() allocates arrays of the specified sizes with CC values initialized to 64 (center for axes).
File: PadForge.Engine/Common/VirtualControllerTypes.cs
public enum VirtualControllerType
{
Xbox360 = 0,
DualShock4 = 1,
VJoy = 2,
Midi = 3
}File: PadForge.Engine/Common/VirtualControllerTypes.cs
public interface IVirtualController : IDisposable
{
VirtualControllerType Type { get; }
bool IsConnected { get; }
int FeedbackPadIndex { get; set; }
void Connect();
void Disconnect();
void SubmitGamepadState(Gamepad gp);
void RegisterFeedbackCallback(int padIndex, Vibration[] vibrationStates);
}FeedbackPadIndex tracks which slot this VC occupies so feedback callbacks write to the correct VibrationStates element after a slot reorder via SwapSlotData.
Abstraction over virtual controller operations. Concrete implementations live in the App assembly:
-
Xbox360VirtualController(ViGEm) -
DS4VirtualController(ViGEm) -
VJoyVirtualController(direct P/Invoke tovJoyInterface.dll) -
MidiVirtualController(Windows MIDI Services SDK)
File: PadForge.Engine/Common/CustomInputState.cs
API-agnostic snapshot of a device's complete input state at a single point in time.
public class CustomInputState
{
public const int MaxAxis = 24;
public const int MaxSliders = 8;
public const int MaxPovs = 4;
public const int MaxButtons = 256;
public int[] Axis; // 0-65535, center = 32767
public int[] Sliders; // 0-65535
public int[] Povs; // centidegrees 0-35900, or -1 for centered
public bool[] Buttons; // true = pressed
public float[] Gyro; // [X, Y, Z] radians per second
public float[] Accel; // [X, Y, Z] meters per second squared
}public CustomInputState()
public CustomInputState(int[] axes, int[] sliders, int[] povs, bool[] buttons)Default constructor creates zeroed arrays with default sizes. POVs initialize to -1 (centered). Copy constructor copies arrays up to max lengths (snapshot isolation).
public CustomInputState Clone()Deep copy of all arrays.
public static void GetAxisMask(
DeviceObjectItem[] items, int numAxes,
out int axisMask, out int actuatorMask, out int actuatorCount)Scans device object items to build axis and force-feedback actuator bitmasks. Bit N set = axis/actuator N exists.
public static int GetSlidersMask(DeviceObjectItem[] items, int numSliders)Scans device object items to build a slider bitmask.
| Array | Range | Center | Description |
|---|---|---|---|
Axis |
0-65535 | 32767 | Indices 0-5 = X, Y, Z, Rx, Ry, Rz. 6-23 = additional |
Sliders |
0-65535 | 32767 | Overflow or dedicated slider controls |
Povs |
0-35900 or -1 | -1 | Centidegrees. -1 = centered |
Buttons |
bool | false | 256 max (covers full Windows VK code range) |
Gyro |
float[3] | 0.0 | Radians/second. Only for gyro-capable devices |
Accel |
float[3] | 0.0 | m/s^2. Only for accelerometer-capable devices |
File: PadForge.Engine/Common/ISdlInputDevice.cs
Common interface for all SDL-based input device wrappers (joystick/gamepad, keyboard, mouse).
public interface ISdlInputDevice : IDisposable
{
// Identity
uint SdlInstanceId { get; }
string Name { get; }
Guid InstanceGuid { get; }
Guid ProductGuid { get; }
string DevicePath { get; }
string SerialNumber { get; }
ushort VendorId { get; }
ushort ProductId { get; }
// Capabilities
int NumAxes { get; }
int NumButtons { get; }
int RawButtonCount { get; }
int NumHats { get; }
bool HasRumble { get; }
bool HasHaptic { get; }
bool HasGyro { get; }
bool HasAccel { get; }
bool IsAttached { get; }
// Haptic
HapticEffectStrategy HapticStrategy { get; }
IntPtr HapticHandle { get; }
uint HapticFeatures { get; }
// State reading
CustomInputState GetCurrentState();
DeviceObjectItem[] GetDeviceObjects();
int GetInputDeviceType();
// Force feedback
bool SetRumble(ushort low, ushort high, uint durationMs = uint.MaxValue);
bool StopRumble();
}public enum HapticEffectStrategy
{
None,
LeftRight,
Sine,
Constant
}Priority order chosen at haptic open time based on SDL_GetHapticFeatures:
- LeftRight — Best match for dual-motor rumble (low + high frequency).
- Sine — Periodic effect. Period varies by which motor is stronger.
- Constant — Fallback. Level from dominant motor.
File: PadForge.Engine/Common/WebControllerDevice.cs
Virtual input device representing a browser-connected gamepad. Implements ISdlInputDevice so it integrates with the standard input pipeline.
| Property | Value |
|---|---|
| VID / PID |
0xBEEF / 0xCA7E
|
| Axes | 6 (LX, LY, LT, RX, RY, RT — 0-65535 range) |
| Buttons | 11 (standard Xbox layout: A, B, X, Y, LB, RB, Back, Start, LS, RS, Guide) |
| POV Hats | 1 |
| HasRumble | true (via browser Vibration API) |
State is written by the WebSocket thread and read by the polling thread via volatile reference swaps.
File: PadForge.Engine/Common/DeviceObjectItem.cs
Describes a single input object (axis, button, hat, slider) on a device. Used by the mapping UI and pipeline.
public class DeviceObjectItem
{
// Identity
public string Name { get; set; }
public Guid ObjectTypeGuid { get; set; }
public DeviceObjectTypeFlags ObjectType { get; set; }
// Position
public int InputIndex { get; set; }
public int Offset { get; set; }
// Aspect
public ObjectAspect Aspect { get; set; }
// Computed helpers
public bool IsForceActuator { get; }
public bool IsAxis { get; }
public bool IsButton { get; }
public bool IsPov { get; }
public bool IsSlider { get; }
}| Property | Type | Default | Description |
|---|---|---|---|
Name |
string |
"" |
Human-readable name (e.g., "X Axis", "Button 3") |
ObjectTypeGuid |
Guid |
Guid.Empty |
Well-known GUID from ObjectGuid
|
ObjectType |
DeviceObjectTypeFlags |
All |
Classification flags |
InputIndex |
int |
0 |
Zero-based index into CustomInputState arrays |
Offset |
int |
0 |
Byte offset for legacy mapping compatibility |
Aspect |
ObjectAspect |
Position |
Position, Velocity, Acceleration, or Force |
-
IsForceActuator:(ObjectType & ForceFeedbackActuator) != 0 -
IsAxis:(ObjectType & Axis) != 0 -
IsButton:(ObjectType & Button) != 0 -
IsPov:(ObjectType & PointOfViewController) != 0 -
IsSlider:ObjectTypeGuid == ObjectGuid.Slider
Returns "{Name} ({TypeLabel}, Index {InputIndex})" where TypeLabel is "Axis", "Slider", "POV", "Button", or "Object".
File: PadForge.Engine/Common/InputTypes.cs
[Flags]
public enum DeviceObjectTypeFlags : int
{
All = 0,
RelativeAxis = 1,
AbsoluteAxis = 2,
Axis = 3, // RelativeAxis | AbsoluteAxis
PushButton = 4,
ToggleButton = 8,
Button = 12, // PushButton | ToggleButton
PointOfViewController = 16,
Collection = 64,
NoData = 128,
ForceFeedbackActuator = 0x01000000,
ForceFeedbackEffectTrigger = 0x02000000
}[Flags]
public enum ObjectAspect : int
{
Position = 0x100,
Velocity = 0x200,
Acceleration = 0x300,
Force = 0x400
}[Flags]
public enum EffectParameterFlags : int
{
None = 0,
Duration = 1,
SamplePeriod = 2,
Gain = 4,
TriggerButton = 8,
TriggerRepeatInterval = 16,
Axes = 32,
Direction = 64,
Envelope = 128,
TypeSpecificParameters = 256,
StartDelay = 512,
AllParameters = 0x3FF,
Start = 0x20000000,
NoRestart = 0x40000000,
NoDownload = unchecked((int)0x80000000)
}Static class providing well-known GUIDs for device object types. Values match DirectInput GUID constants.
public static class ObjectGuid
{
public static readonly Guid XAxis; // {A36D02E0-C9F3-11CF-BFC7-444553540000}
public static readonly Guid YAxis; // {A36D02E1-...}
public static readonly Guid ZAxis; // {A36D02E2-...}
public static readonly Guid RxAxis; // {A36D02F4-...}
public static readonly Guid RyAxis; // {A36D02F5-...}
public static readonly Guid RzAxis; // {A36D02E3-...}
public static readonly Guid Slider; // {A36D02E4-...}
public static readonly Guid Button; // {A36D02F0-...}
public static readonly Guid Key; // {55728220-D33C-11CF-BFC7-444553540000}
public static readonly Guid PovController; // {A36D02F2-...}
public static readonly Guid Unknown; // Guid.Empty
}Integer constants matching DirectInput device type values. Used for UserDevice.CapType.
public static class InputDeviceType
{
public const int Device = 17;
public const int Mouse = 18;
public const int Keyboard = 19;
public const int Joystick = 20;
public const int Gamepad = 21;
public const int Driving = 22;
public const int Flight = 23;
public const int FirstPerson = 24;
public const int Supplemental = 25;
}public enum MapType : int
{
None = 0,
Axis = 1,
Button = 2,
Slider = 3,
POV = 4
}File: PadForge.Engine/Common/ForceFeedbackState.cs
Manages force feedback (rumble) state for a single device with change detection.
public class ForceFeedbackState
{
// Public state
public ushort LeftMotorSpeed { get; } // 0-65535
public ushort RightMotorSpeed { get; } // 0-65535
public bool IsActive { get; }
// Methods
public void SetDeviceForces(UserDevice ud, ISdlInputDevice device, PadSetting ps, Vibration v);
public void StopDeviceForces(ISdlInputDevice device);
public bool Changed(PadSetting ps);
}public void SetDeviceForces(UserDevice ud, ISdlInputDevice device, PadSetting ps, Vibration v)- Reads gain settings from
PadSetting:ForceOverall,LeftMotorStrength,RightMotorStrength(all 0-100). - Applies per-motor and overall gain scaling to raw XInput motor speeds.
- Swaps motors if
ForceSwapMotoris configured. -
Change detection: Only sends to hardware when values differ from cached values. Each
SDL_RumbleJoystickcall restarts the hardware rumble, causing brief gaps. By only sending on change withuint.MaxValueduration (~49 days), rumble stays continuous. - Routes to either
SetHapticForces()(for haptic devices) orSetRumble()(for rumble devices).
private bool SetHapticForces(ISdlInputDevice device, ushort left, ushort right)Translates dual-motor rumble to SDL haptic effects based on the device's HapticEffectStrategy:
| Strategy | SDL Effect | Large Motor | Small Motor |
|---|---|---|---|
| LeftRight | SDL_HAPTIC_LEFTRIGHT |
large_magnitude = left |
small_magnitude = right |
| Sine | SDL_HAPTIC_SINE |
magnitude = max/2, period = 120
|
period = 40 |
| Constant | SDL_HAPTIC_CONSTANT |
level = max/2 |
N/A |
Creates effect on first call via SDL_CreateHapticEffect, updates in-place on subsequent calls via SDL_UpdateHapticEffect.
public bool Changed(PadSetting ps)Returns true if any force feedback setting in the PadSetting has changed since last call. Caches: ForceType, ForceSwapMotor, LeftMotorStrength, RightMotorStrength, ForceOverall.
File: PadForge.Engine/Common/ForceFeedbackState.cs
public class Vibration
{
public ushort LeftMotorSpeed { get; set; } // Low-frequency, heavy rumble. 0-65535
public ushort RightMotorSpeed { get; set; } // High-frequency, light buzz. 0-65535
public Vibration();
public Vibration(ushort leftMotor, ushort rightMotor);
}Matches the XINPUT_VIBRATION layout. Populated from ViGEm FeedbackReceived callback on the ViGEm thread; read by Step 2 on the polling thread.
File: PadForge.Engine/Common/RumbleLogger.cs
public static class RumbleLogger
{
public static bool Enabled { get; set; }
public static void Log(string message);
}Diagnostic logger for force feedback debugging. Disabled by default. Set Enabled = true in InputService.Start() to activate. Thread-safe. Writes timestamped messages to a log file using Stopwatch for high-resolution timing.
File: PadForge.Engine/Common/InputHookManager.cs
Manages WH_KEYBOARD_LL and WH_MOUSE_LL low-level Windows hooks for suppressing mapped inputs from keyboards and mice. Only suppresses inputs that are in the active suppression sets — non-mapped keys/buttons pass through normally.
public class InputHookManager : IDisposable
{
void Start();
void Stop();
void SetSuppressedKeys(HashSet<int> vkCodes);
void SetSuppressedMouseButtons(HashSet<int> buttons);
bool HasAnySuppression { get; }
}Hooks require a thread with a message pump. InputHookManager creates a dedicated background thread running a GetMessage loop. Hooks are installed on this thread via SetWindowsHookExW.
-
Start()creates the thread and waits (viaManualResetEventSlim) until hooks are installed before returning. -
Stop()postsWM_QUITto the hook thread to exit the message loop, then joins the thread.
Suppression sets use volatile reference swap for thread safety — the hook callbacks read the current set reference without locking, and updates replace the entire HashSet atomically.
void SetSuppressedKeys(HashSet<int> vkCodes) // Virtual key codes to suppress
void SetSuppressedMouseButtons(HashSet<int> buttons) // 0=Left, 1=Right, 2=Middle, 3=XButton1, 4=XButton2-
Keyboard: Intercepts
WM_KEYDOWN,WM_KEYUP,WM_SYSKEYDOWN,WM_SYSKEYUP. ReadsKBDLLHOOKSTRUCT.vkCodeand checks against the suppression set. Returns(IntPtr)1to suppress, orCallNextHookExto pass through. -
Mouse: Intercepts button messages (
WM_LBUTTONDOWN/UP,WM_RBUTTONDOWN/UP,WM_MBUTTONDOWN/UP,WM_XBUTTONDOWN/UP). Converts the message to a button ID viaMouseMessageToButtonId()and checks against the suppression set. Mouse movement and wheel events always pass through.
| Mouse Message | Button ID |
|---|---|
WM_LBUTTONDOWN/UP |
0 (Left) |
WM_RBUTTONDOWN/UP |
1 (Right) |
WM_MBUTTONDOWN/UP |
2 (Middle) |
WM_XBUTTONDOWN/UP (XBUTTON1) |
3 |
WM_XBUTTONDOWN/UP (XBUTTON2) |
4 |
| Other (move, wheel) | -1 (pass through) |
| Function | DLL | Purpose |
|---|---|---|
SetWindowsHookExW |
user32.dll | Install low-level hook |
UnhookWindowsHookEx |
user32.dll | Remove hook |
CallNextHookEx |
user32.dll | Pass input to next hook |
GetModuleHandleW |
kernel32.dll | Get module handle for hook registration |
GetMessageW |
user32.dll | Message pump loop |
PostThreadMessageW |
user32.dll | Post WM_QUIT to hook thread |
GetCurrentThreadId |
kernel32.dll | Get hook thread ID |
| Field | Type | Description |
|---|---|---|
LeftThumbCenterOffsetX |
string |
Left stick X center offset (-100 to 100%). Default "0". |
LeftThumbCenterOffsetY |
string |
Left stick Y center offset. Default "0". |
RightThumbCenterOffsetX |
string |
Right stick X center offset. Default "0". |
RightThumbCenterOffsetY |
string |
Right stick Y center offset. Default "0". |
LeftThumbMaxRangeX |
string |
Left stick X max range (1-100%). Default "100". |
LeftThumbMaxRangeY |
string |
Left stick Y max range. Default "100". |
RightThumbMaxRangeX |
string |
Right stick X max range. Default "100". |
RightThumbMaxRangeY |
string |
Right stick Y max range. Default "100". |
Center offset is applied in Step 3 before dead zone processing. Max range scales the output range.
| Field | Type | Description |
|---|---|---|
HidHideEnabled |
bool |
Whether this device is hidden from games via HidHide |
ConsumeInputEnabled |
bool |
Whether mapped keyboard/mouse inputs are suppressed via hooks |
ForceRawJoystickMode |
bool |
Bypass SDL3 gamepad remapping, read raw joystick indices |
HidHideInstanceIds |
List<string> |
Cached HID instance IDs for blacklisting (persisted for offline devices) |