npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

xinput-ffi

v4.0.1

Published

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

Downloads

93

Readme

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

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]});
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 }
}
*/
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
  }
}
*/
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

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({dwUserIndex: 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({dwUserIndex: 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_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.