Skip to content

xan105/node-xinput-ffi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

About

Access native XInput functions as well as some helpers based around them.

This lib hooks directly to the system's dll (xinput1_4.dll, xinput1_3.dll or xinput9_1_0.dll).
It aims to implement and expose XInput functions as close as possible to the document.

πŸ” "Hidden" XInput functions such as XInputGetCapabilitiesEx() are exposed as well.

Examples

Vibration via helper function
import { rumble } from "xinput-ffi";

//Rumble 1st XInput gamepad
await rumble();

//Now with 100% force
await rumble({force: 100}); 

//low-frequency rumble motor(left) at 50% 
//and high-frequency rumble motor (right) at 25%
await rumble({force: [50,25]});
XInput function
import * as XInput from "xinput-ffi";

const capabilities = await XInput.getCapabilities({translate: true});
console.log(capabilities);
/* Output:
{
  type: 'XINPUT_DEVTYPE_GAMEPAD',
  subType: 'XINPUT_DEVSUBTYPE_GAMEPAD',
  flags: [ 'XINPUT_CAPS_VOICE_SUPPORTED', 'XINPUT_CAPS_PMD_SUPPORTED' ],
  gamepad: {
    wButtons: [
      'XINPUT_GAMEPAD_DPAD_UP',
      'XINPUT_GAMEPAD_DPAD_DOWN',
      //etc...
    ],
    bLeftTrigger: 255,
    bRightTrigger: 255,
    sThumbLX: -64,
    sThumbLY: -64,
    sThumbRX: -64,
    sThumbRY: -64
  },
  vibration: { wLeftMotorSpeed: 255, wRightMotorSpeed: 255 }
}
*/
"Hidden" XInput function
import * as XInput from "xinput-ffi";

const state = await XInput.getStateEx();
console.log(state);
/*Output:
{
  dwPacketNumber: 6510,
  gamepad: {
    wButtons: [ 'XINPUT_GAMEPAD_GUIDE' ],
    bLeftTrigger: 0,
    bRightTrigger: 0,
    sThumbLX: -1024,
    sThumbLY: 767,
    sThumbRX: 257,
    sThumbRY: 767
  }
}
*/
Miscellaneous
import * as XInput from "xinput-ffi";

//Check connected status for all controller
console.log(await XInput.listConnected());
// [true,false,false,false] Only 1st gamepad is connected
  
//Identify connected XInput devices
console.log (await XInput.identify({XInputOnly: true})); 
/* Output:
  [
    {
      name: 'Xbox360 Controller',
      manufacturer: 'Microsoft Corp.',
      vendorID: 1118,
      productID: 654,
      xinput: true,
      interfaces: [ 'USB', 'HID' ],
      guid: [
        '{745a17a0-74d3-11d0-b6fe-00a0c90f57da}',
        '{d61ca365-5af4-4486-998b-9db4734c6ca3}'
      ]
    }
  ]
*/

Electron

Simple XInput menu navigation

Here is an example of a simple XInput menu navigation system using the high level XInput implementation found in this module (helper).

  • main process
let gamepad;

mainWin.once("ready-to-show", async() => { 

  const { XInputGamepad } = await import("xinput-ffi");
  gamepad = new XInputGamepad();
  
  //send input to renderer
  gamepad.on("input", (buttons)=>{ 
    setImmediate(() => {
      mainWin.webContents.send("onGamepadInput", buttons); 
    });
  });
  
  gamepad.poll(); //gamepad event loop
  mainWin.show();
  mainWin.focus();
  
});

//gain/loose focus
mainWin.on("blur", () => {
  gamepad?.pause();
});
mainWin.on("focus", () => {
  gamepad?.resume();
});

//clean up
mainWin.on("close", () => {
  gamepad?.stop();
  gamepad = null; //deref
});

mainWin.on("closed", () => {
  mainWin = null; //deref
});

mainWin.loadFile(path/to/file);
  • contextBridge (preload)
contextBridge.exposeInMainWorld("ipcRenderer", {
  onGamepadInput: (callback) => ipcRenderer.on("onGamepadInput", callback)
});
  • renderer
window.ipcRenderer.onGamepadInput((event, input) => {
    switch(input[0]){
      case "XINPUT_GAMEPAD_DPAD_UP":
        //do something
        break;
      default:
        console.log(input);
    }
  });

Installation

npm install xinput-ffi

API

⚠️ This module is only available as an ECMAScript module (ESM) starting with version 2.0.0.
Previous version(s) are CommonJS (CJS) with an ESM wrapper.

Named export

Summary:

const constants = object

XInput controller constants for convenience.

  import { constants } from "xinput-ffi";
  console.log(constants.XUSER_MAX_COUNT); //4

πŸ’‘ Also available under its own namespace.

  import { XUSER_MAX_COUNT } from "xinput-ffi/constants";
  console.log(XUSER_MAX_COUNT); //4

XInput function

πŸ“– Microsoft documentation

"Hidden" and undocumented functions
πŸ“– Reverse Engineer's log

  • βœ”οΈ XInputGetStateEx
  • βœ”οΈ XInputWaitForGuideButton
  • βœ”οΈ XInputCancelGuideButtonWait
  • βœ”οΈ XInputPowerOffController
  • ⚠️ XInputGetBaseBusInformation > Not working with all gamepad.
  • βœ”οΈ XInputGetCapabilitiesEx

NB: Depending on which XInput dll version you are using (1_4, 1_3, 9_1_0) some functions won't be available.

enable(enable: boolean): Promise<void>

Enable/Disable all XInput gamepads.
This function is meant to be called when an application gains or loses focus.

NB:

  • Stop any rumble currently playing when set to false.
  • This may trigger ERR_DEVICE_NOT_CONNECTED for set/getState(Ex) when set to false and there was no prior input ever.

πŸ“– XInputEnable

getBatteryInformation(option?: number | object): Promise<object>

Retrieves the battery type and charge status of a wireless controller.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • devType?: number (0)

Specifies which device associated with this controller should be queried.
0: GAMEPAD or 1: HEADSET

  • translate?: boolean (true)

When a value is known it will be 'translated' to its string equivalent value otherwise its integer value.
If you want the raw data only set it to false.

πŸ’‘ If option is a number it will be used as dwUserIndex.

Returns an object like a πŸ“– XINPUT_BATTERY_INFORMATION structure.

Example

await getBatteryInformation();
await getBatteryInformation(0);
await getBatteryInformation({dwUserIndex: 0});
//output
{
  batteryType: 'BATTERY_TYPE_WIRED',
  batteryLevel: 'BATTERY_LEVEL_FULL'
}

If you want raw data output

await getBatteryInformation({translate: false});
//output
{
  batteryType: 1,
  batteryLevel: 3
}

πŸ“– XInputGetBatteryInformation

getCapabilities(option?: number | object): Promise<object>

Retrieves the capabilities and features of the specified controller.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • dwFlags?: number (1)

Input flags that identify the controller type.
If this value is 0, then the capabilities of all controllers connected to the system are returned.
Currently, only 1: XINPUT_FLAG_GAMEPAD is supported.

  • translate?: boolean (true)

When a value is known it will be 'translated' to its string equivalent value otherwise its integer value.
If you want the raw data only set it to false.

πŸ’‘ If option is a number it will be used as dwUserIndex.

Returns an object like a πŸ“– XINPUT_CAPABILITIES structure.

Example

await getCapabilities();
await getCapabilities(0);
await getCapabilities({gamepadIndex: 0});
//Output
{
  type: 'XINPUT_DEVTYPE_GAMEPAD',
  subType: 'XINPUT_DEVSUBTYPE_GAMEPAD',
  flags: [ 'XINPUT_CAPS_VOICE_SUPPORTED', 'XINPUT_CAPS_PMD_SUPPORTED' ],
  gamepad: {
    wButtons: [
      'XINPUT_GAMEPAD_DPAD_UP',
      'XINPUT_GAMEPAD_DPAD_DOWN',
      'XINPUT_GAMEPAD_DPAD_LEFT',
      'XINPUT_GAMEPAD_DPAD_RIGHT',
      'XINPUT_GAMEPAD_START',
      'XINPUT_GAMEPAD_BACK',
      'XINPUT_GAMEPAD_LEFT_THUMB',
      'XINPUT_GAMEPAD_RIGHT_THUMB',
      'XINPUT_GAMEPAD_LEFT_SHOULDER',
      'XINPUT_GAMEPAD_RIGHT_SHOULDER',
      'XINPUT_GAMEPAD_A',
      'XINPUT_GAMEPAD_B',
      'XINPUT_GAMEPAD_X',
      'XINPUT_GAMEPAD_Y'
    ],
    bLeftTrigger: 255,
    bRightTrigger: 255,
    sThumbLX: -64,
    sThumbLY: -64,
    sThumbRX: -64,
    sThumbRY: -64
  },
  vibration: { wLeftMotorSpeed: 255, wRightMotorSpeed: 255 }
}

If you want raw data output

await getCapabilities({translate: false});
//output
{
  type: 1,
  subType: 1,
  flags: 12,
  gamepad: {
    wButtons: 65535,
    bLeftTrigger: 255,
    bRightTrigger: 255,
    sThumbLX: -64,
    sThumbLY: -64,
    sThumbRX: -64,
    sThumbRY: -64
  },
  vibration: { wLeftMotorSpeed: 255, wRightMotorSpeed: 255 }
}

πŸ“– XInputGetCapabilities

getKeystroke(option?: number | object): Promise<object>

Retrieves a gamepad input event.
To be honest, this isn't really useful since the chatpad feature wasn't implemented on Windows.
⚠️ NB: If no new keys have been pressed, this will throw with ERROR_EMPTY.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • translate?: boolean (true)

When a value is known it will be 'translated' to its string equivalent value otherwise its integer value.
If you want the raw data only set it to false.

πŸ’‘ If option is a number it will be used as dwUserIndex.

Returns an object like a πŸ“– XINPUT_KEYSTROKE structure.

Example

await getKeystroke();
await getKeystroke(0);
await getKeystroke({dwUserIndex: 0});
//Output
{
  virtualKey: 'VK_PAD_A',
  unicode: 0,
  flags: [ 'XINPUT_KEYSTROKE_KEYDOWN' ],
  userIndex: 0,
  hidCode: 0
}

If you want raw data output

await getKeystroke({translate: false});
//output
{ 
  virtualKey: 22528, 
  unicode: 0, 
  flags: 1, 
  userIndex: 0, 
  hidCode: 0 
}

πŸ“– XInputGetKeystroke

getState(option?: number | object): Promise<object>

Retrieves the current state of the specified controller.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • translate?: boolean (true)

When a value is known it will be 'translated' to its string equivalent value otherwise its integer value.
If you want the raw data only set it to false.

πŸ’‘ If option is a number it will be used as dwUserIndex.

Returns an object like a πŸ“– XINPUT_STATE structure.

Example

await getState();
await getState(0);
await getState({dwUserIndex: 0});
//Output
{
  dwPacketNumber: 18165,
  gamepad: { 
    wButtons: ['XINPUT_GAMEPAD_A'],
    bLeftTrigger: 0,
    bRightTrigger: 0,
    sThumbLX: 128,
    sThumbLY: 641,
    sThumbRX: -1156,
    sThumbRY: -129
  }
}

If you want raw data output

await getState({translate: false});
//output
{
  dwPacketNumber: 322850,
  gamepad: {
    wButtons: 4096,
    bLeftTrigger: 0,
    bRightTrigger: 0,
    sThumbLX: 257,
    sThumbLY: 767,
    sThumbRX: 773,
    sThumbRY: 1279
  }
}

πŸ’‘ Thumbsticks: as explained by Microsoft you should implement dead zone correctly.
This is done for you in getButtonsDown()

πŸ“– XInputGetState

setState(lowFrequency: number, highFrequency: number, option ?: number | object): Promise<void>

Sends data to a connected controller. This function is used to activate the vibration function of a controller.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • usePercent?: boolean (true)

XInputSetState valid values are in the range 0 to 65535.
Zero signifies no motor use; 65535 signifies 100 percent motor use.
lowFrequency and highFrequency are in % (0-100) for convenience when you set this to true.

πŸ’‘ If option is a number it will be used as dwUserIndex.

NB:

  • You need to keep the event-loop alive otherwise the vibration will terminate with your program.
  • You need to reset the state to 0 for both frequency before using setState again.

Both are done for you with rumble()

πŸ“– XInputSetState

getStateEx(option?: number | object): Promise<object>

The same as XInputGetState, adding the "Guide" button (0x0400).

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • translate?: boolean (true)

When a value is known it will be 'translated' to its string equivalent value otherwise its integer value.
If you want the raw data only set it to false.

πŸ’‘ If option is a number it will be used as dwUserIndex.

Returns an object like a πŸ“– XINPUT_STATE structure.

Example

await getStateEx();
await getStateEx(0);
await getStateEx({dwUserIndex: 0});
//Output
{
  dwPacketNumber: 18165,
  gamepad: { 
    wButtons: ['XINPUT_GAMEPAD_GUIDE'],
    bLeftTrigger: 0,
    bRightTrigger: 0,
    sThumbLX: 128,
    sThumbLY: 641,
    sThumbRX: -1156,
    sThumbRY: -129
  }
}

If you want raw data output

await getStateEx({translate: false});
//output
{
  dwPacketNumber: 322850,
  gamepad: {
    wButtons: 1024,
    bLeftTrigger: 0,
    bRightTrigger: 0,
    sThumbLX: 257,
    sThumbLY: 767,
    sThumbRX: 773,
    sThumbRY: 1279
  }
}

waitForGuideButton(option?: number | object): Promise<void>

Wait until Guide button is pressed.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • dwFlags?: number (0)

Wait behavior:
0: Blocking 1: Async
It's not clear on how to get the async option to report.

πŸ’‘ If option is a number it will be used as dwUserIndex.

cancelGuideButtonWait(option?: number | object): Promise<void>

If XInputWaitForGuideButton was activated in async mode, this will stop it.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

πŸ’‘ If option is a number it will be used as dwUserIndex.

powerOffController(option?: number | object): Promise<void>

Power off a controller.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

πŸ’‘ If option is a number it will be used as dwUserIndex.

getBaseBusInformation(option?: number | object): Promise<object>

⚠️ Not working on all gamepads. It can refuse and return ERROR_DEVICE_NOT_CONNECTED, even if connected.

βš™οΈ options:

  • dwBusIndex?: number (0)

Bus index. Can be a value from 0 to 16.

πŸ’‘ If option is a number it will be used as dwBusIndex?.

Returns an object like the following structure:

struct XINPUT_BASE_BUS_INFORMATION
{
  WORD VendorId, //unknown
  WORD ProductId, //unknown
  WORD InputId, //unknown
  WORD Field_6, //unknown
  DWORD Field_8, //unknown
  BYTE Field_C, //unknown
  BYTE Field_D, //unknown
  BYTE Field_E, //unknown
  BYTE Field_F //unknown
 }

getCapabilitiesEx(option?: number | object): Promise<object>

The same as XInputGetCapabilities but with added properties such as vendorID and productID.

βš™οΈ options:

  • dwUserIndex?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • translate?: boolean (true)

When a value is known it will be 'translated' to its string equivalent value otherwise its integer value.
If you want the raw data only set it to false.

πŸ’‘ If option is a number it will be used as dwUserIndex.

Returns an object similar to πŸ“– XINPUT_CAPABILITIES structure.
See below for details.

Example

await getCapabilitiesEx();
await getCapabilitiesEx(0);
await getCapabilitiesEx({gamepadIndex: 0});
//Output
{
  capabilities: {
    type: 'XINPUT_DEVTYPE_GAMEPAD',
    dubType: 'XINPUT_DEVSUBTYPE_GAMEPAD',
    flags: [ 'XINPUT_CAPS_VOICE_SUPPORTED', 'XINPUT_CAPS_PMD_SUPPORTED' ],
    gamepad: {
      wButtons: [
        'XINPUT_GAMEPAD_DPAD_UP',
        'XINPUT_GAMEPAD_DPAD_DOWN',
        'XINPUT_GAMEPAD_DPAD_LEFT',
        'XINPUT_GAMEPAD_DPAD_RIGHT',
        'XINPUT_GAMEPAD_START',
        'XINPUT_GAMEPAD_BACK',
        'XINPUT_GAMEPAD_LEFT_THUMB',
        'XINPUT_GAMEPAD_RIGHT_THUMB',
        'XINPUT_GAMEPAD_LEFT_SHOULDER',
        'XINPUT_GAMEPAD_RIGHT_SHOULDER',
        'XINPUT_GAMEPAD_A',
        'XINPUT_GAMEPAD_B',
        'XINPUT_GAMEPAD_X',
        'XINPUT_GAMEPAD_Y'
      ],
      bLeftTrigger: 255,
      bRightTrigger: 255,
      sThumbLX: -64,
      sThumbLY: -64,
      sThumbRX: -64,
      sThumbRY: -64
    },
    vibration: { wLeftMotorSpeed: 255, wRightMotorSpeed: 255 }
  },
  vendorId: 'Microsoft Corp.',
  productId: 'Xbox360 Controller',
  productVersion: 276,
}

If you want raw data output

await getCapabilitiesEx({translate: false});
//output
{
  capabilities: {
    type: 1,
    dubType: 1,
    flags: 12,
    gamepad: {
      wButtons: 62463,
      bLeftTrigger: 255,
      bRightTrigger: 255,
      sThumbLX: -64,
      sThumbLY: -64,
      sThumbRX: -64,
      sThumbRY: -64
    },
    vibration: { 
      wLeftMotorSpeed: 255, 
      wRightMotorSpeed: 255 
    }
  },
  vendorId: 1118,
  productId: 654,
  productVersion: 276,
}

Helper functions

The following are sugar/helper functions based upon the previous XInput functions.

isConnected(gamepad?: number): Promise<boolean>

Whether the specified controller is connected or not.
Returns true/false.

listConnected(): Promise<boolean[]>

Returns an array of connected status for all controller.
eg: [true,false,false,false] => Only 1st gamepad is connected

getButtonsDown(option?: object): Promise<object>

Normalize getState()/getStateEx() information for convenience:
ThumbStick position, magnitude, direction (taking the deadzone into account).
Trigger state and force (taking threshold into account).
Which buttons are pressed if any.

βš™οΈ options:

  • gamepad?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • deadzone?: number | number[] ( [7849,8689] )

Thumbstick deadzone(s):
Either an integer (both thumbstick with the same value) or an array of 2 integer: [left,right]

  • directionThreshold?: number (0.2)

float [0.0,1.0] to handle cardinal direction.
Set it to 0 so direction[] only reports "UP RIGHT", "UP LEFT", "DOWN LEFT", "DOWN RIGHT".
Otherwise "RIGHT", "LEFT", "UP", "DOWN" will be added to the above using threshold to
differentiate the 2 axes by using range of [-threshold,threshold].

πŸ’‘ If you just want "RIGHT", "LEFT", "UP" and "DOWN" the easiest way is to set this to 0.8 with the default deadzone.
Alternatively play with this value and/or deadzone to decide on a thresold and ignore when direction[] has a length of 2.

  • triggerThreshold?: number (30)

Trigger activation threshold. Range [0,255].

=> Returns an object where:

  • int packetNumber : dwPacketNumber; This value is increased every time the state of the controller has changed.
  • []string buttons : list of currently pressed buttons
  • trigger.left/right :
    • boolean active : is the trigger pressed down ? (below triggerThreshold will not set active to true)
    • int force : by how much ? [0,255]
  • thumb.left/right :
    • float x: normalized (deadzone) x axis [0.0,1.0]. 0 is centered. Negative values is left. Positive values is right.
    • float y: normalized (deadzone) y axis [0.0,1.0]. 0 is centered. Negative values is down. Positive values is up.
    • float magnitude: normalized (deadzone) magnitude [0.0,1.0] (by how far is the thumbstick from the center ? 1 is fully pushed).
    • []string direction: Human readable direction of the thumbstick. eg: ["UP", "RIGHT"]. See directionThreshold above for details.
{
  packetNumber: 132309,
  buttons: [ 'XINPUT_GAMEPAD_A' ],
  trigger: {
    left: { active: true, force: 255 },
    right: { active: false, force: 0 }
  },
  thumb: {
    left: {
      x: -0.6960457056589758,
      y: 0.717997476063599,
      magnitude: 1,
      direction: [ 'UP', 'LEFT' ]
    },
    right: {
      x: 0.039307955814283674,
      y: 0.9992271436513833,
      magnitude: 1,
      direction: [ 'UP' ]
    }
  }
}

rumble(option?: object): Promise<void>

This function is used to activate the vibration function of a controller.

βš™οΈ options:

  • gamepad?: number (0)

Index of the user's controller. Can be a value from 0 to 3.

  • force?: number | number[] ([50,25])

Vibration force in % (0-100) to apply to the motors.
Either an integer (both motor with the same value) or an array of 2 integer: [left,right]

  • duration?: number (2500)

Vibration duration in ms. Max: ~2500 ms.

  • forceEnableGamepad?: boolean (false)

Use enable() to force the activation of XInput gamepad before vibration.

  • forceStateWhileRumble?: boolean (false)

Bruteforce -ly (spam) setState() for the duration of the vibration. Use this when a 3rd party reset your state or whatever.
⚠️ Usage of this option is not recommended use only when needed.

Identify device | VID/PID

XInput doesn't provide VID/PID by design.
Even if with XInputGetCapabilitiesEx you can get the vendorID and productID, it will most likely be a Xbox Controller (real one or through XInput emulation).

Use identify() (see below) to query WMI _Win32_PNPEntity to scan for known gamepads.
It won't tell you which is connected to which XInput slot tho.

identify(option?: object): Promise<object[]>

⚠️ Requires PowerShell.

List all known HID and USB connected devices by matching with entries in ./lib/util/HardwareID.js

βš™οΈ options:

  • XInputOnly?: boolean (true)

Return only XInput gamepad.

=> Return an array of object where

  • string name : device name
  • string manufacturer : vendor name
  • number vendorID : vendor id
  • number productID : product id
  • string[] interfaces : PNPentity interface(s) found; Available: HID and USB
  • string[] guid: classguid(s) found
  • boolean xinput: a XInput device or not

πŸ’‘ object are unique by their vid/pid

Output example with a DS4(wireless) and ds4windows(XInput wrapper):

import { identify } from "xinput-ffi";
await identify();
//Output
[
  {
    name: 'DualShock 4 (v2)',
    manufacturer: 'Sony Corp.',
    vendorID: 1356,
    productID: 2508,
    xinput: false,
    interfaces: [ 'USB', 'HID' ],
    guid: [
      '{36fc9e60-c465-11cf-8056-444553540000}',
      '{745a17a0-74d3-11d0-b6fe-00a0c90f57da}',
      '{4d36e96c-e325-11ce-bfc1-08002be10318}'
    ]
  },
  {
    name: 'DualShock 4 USB Wireless Adaptor',
    manufacturer: 'Sony Corp.',
    vendorID: 1356,
    productID: 2976,
    xinput: false,
    interfaces: [ 'USB', 'HID' ],
    guid: [
      '{745a17a0-74d3-11d0-b6fe-00a0c90f57da}',
      '{36fc9e60-c465-11cf-8056-444553540000}',
      '{4d36e96c-e325-11ce-bfc1-08002be10318}'
    ]
  },
  {
    name: 'Xbox360 Controller',
    manufacturer: 'Microsoft Corp.',
    vendorID: 1118,
    productID: 654,
    xinput: true,
    interfaces: [ 'USB', 'HID' ],
    guid: [
      '{745a17a0-74d3-11d0-b6fe-00a0c90f57da}',
      '{d61ca365-5af4-4486-998b-9db4734c6ca3}'
    ]
  }
]

High level implementation of XInput

This is a high level implementation of XInput to get the gamepad's input on the fly in a human readable way. This serves as an example to demonstrate how to use the XInput functions and helpers based around them. The purpose of this class is to drive a simple navigation menu system with a XInput compatible controller (real XInput or through XInput emulation).

This leverages the new Node.js timersPromises setInterval() to keep the event loop alive and do the gamepad polling.

XInputGamepad(option: object): Class

This class extends EventEmitter from node:events

Options

  • hz?: number (30)

    This will determinate the polling rate. Usually 60hz (1000/60 = ~16ms) is used. If I'm not mistaken this is what the Chrome browser uses. But for our use case we don't need to poll that fast so it defaults to 30hz (~33ms). Increasing this value improves latency, but may cause a loss in performance due to more CPU time spent. The max accepted is 250hz (4ms).

  • multitap?: boolean (true)

    Scan for all 4 XInput slots to find any Gamepad. Set to false to only poll XInput slot 0 and potentially reduce the number of FFI calls per gamepad tick (event loop).

  • joystickAsDPAD?: boolean (true)

    Convert the left joystick analog axis to DPAD buttons. For our use case, driving a simple navigation menu, this is useful.

  • inputFeedback?: boolean (false)

    Vibrate shortly and lightly on any button activation. This is just for fun and/or debug.

Events

input(buttons: string[])

List of activated buttons (human readable) of the first controller found.
A button is "activated" on press (button down) then release (button up).

πŸ’‘ NB: Triggers axis are converted into non standard XInput button name : GAMEPAD_LEFT_TRIGGER and GAMEPAD_RIGHT_TRIGGER (on/off behavior).

XInput Button names:
"XINPUT_GAMEPAD_DPAD_UP",
"XINPUT_GAMEPAD_DPAD_DOWN",
"XINPUT_GAMEPAD_DPAD_LEFT",
"XINPUT_GAMEPAD_DPAD_RIGHT",
"XINPUT_GAMEPAD_START",
"XINPUT_GAMEPAD_BACK",
"XINPUT_GAMEPAD_LEFT_THUMB",
"XINPUT_GAMEPAD_RIGHT_THUMB",
"XINPUT_GAMEPAD_LEFT_SHOULDER",
"XINPUT_GAMEPAD_RIGHT_SHOULDER",
"XINPUT_GAMEPAD_GUIDE",
"XINPUT_GAMEPAD_A",
"XINPUT_GAMEPAD_B",
"XINPUT_GAMEPAD_X",
"XINPUT_GAMEPAD_Y"

πŸ’‘ NB: XInput constants are available under the constants namespace.

import { BUTTONS } from "xinput-ffi/constants";
//or
import { constants } from "xinput-ffi"

Example:

import { XInputGamepad } from "xinput-ffi";

const gamepad = new XInputGamepad({ hz: 60 });

gamepad.on("input", (buttons)=>{ 
  setImmediate(() => {
    console.log(buttons);
  });
});

gamepad.poll();

Methods

poll()

Start the gamepad event loop. This will keep the Node.js event loop going.

❌ Will throw on unexpected error.

stop()

Stop the gamepad event loop.

NB: This method will remove every event listener.

pause()

This function is meant to be called when an application loses focus.

cf: XInputEnable

resume()

This function is meant to be called when an application gains focus.

cf: XInputEnable

vibrate(option: object): Promise<void>

Vibrate the first controller found. Shorthand to the helper fn rumble().

πŸ’‘ Expose only force and duration options of rumble().

❌ Will throw on error other than ERROR_DEVICE_NOT_CONNECTED.