Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154import { spawn } from 'node:child_process';import { createInterface } from 'node:readline';import { fileURLToPath } from 'node:url';import { readFileSync } from 'node:fs';import { neutral, padPose } from './dist/controls.mjs';
const bitMasks = [ 0x1000, 0x2000, 0x4000, 0x8000, 0x100, 0x200, 0, 0, 0x20, 0x10, 0x40, 0x80, 1, 2, 4, 8, 0,];export function launchXInput(once = false) { // Send the local helper as inline commands; no execution-policy or machine settings change. const source = readFileSync(fileURLToPath(new URL('./xinput.ps1', import.meta.url)), 'utf8'); const child = spawn( 'powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '-'], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] }, ); child.stdin.end('& {\n' + source + '\n}' + (once ? ' -Once' : '') + '\n\n'); return child;}export function normalizeXInput(frame) { if (!frame.connected) return null; const axis = (value) => Math.max(-1, Math.min(1, value / (value < 0 ? 32768 : 32767))); const buttons = bitMasks.map((mask) => { const value = mask && frame.buttons & mask ? 1 : 0; return { value, pressed: !!value }; }); buttons[6] = { value: frame.leftTrigger / 255, pressed: frame.leftTrigger > 127 }; buttons[7] = { value: frame.rightTrigger / 255, pressed: frame.rightTrigger > 127 }; return { index: frame.index, axes: [axis(frame.leftX), -axis(frame.leftY), axis(frame.rightX), -axis(frame.rightY)], buttons, mapping: 'standard', };}
export class NativeController { constructor({ onPose = () => {}, onStatus = () => {}, onCamera = () => {}, clock = () => performance.now(), } = {}) { this.onPose = onPose; this.onStatus = onStatus; this.onCamera = onCamera; this.clock = clock; this.settings = { sensitivity: 1, deadzone: 0.12, behavior: 'spring', invert: false }; this.enabled = true; this.available = false; this.error = null; this.pad = null; this.pose = neutral(); this.lastTick = clock(); this.lastFrame = 0; this.menuDown = false; this.wasActive = false; } get active() { return this.available && !!this.pad && this.enabled; } status() { return { backend: 'xinput', available: this.available, connected: !!this.pad, enabled: this.enabled, index: this.pad?.index ?? null, error: this.error, settings: { ...this.settings }, }; } accept(frame) { const previous = this.pad?.index ?? null, wasAvailable = this.available, wasConnected = !!this.pad; this.available = true; this.error = null; this.lastFrame = this.clock(); this.pad = normalizeXInput(frame); if ( previous !== (this.pad?.index ?? null) || wasAvailable !== this.available || wasConnected !== !!this.pad ) this.onStatus(this.status()); } configure(data) { if (data.reset === true) { this.pose = neutral(); this.onPose(this.pose); } if (typeof data.enabled === 'boolean') this.enabled = data.enabled; if (Number.isFinite(data.sensitivity)) this.settings.sensitivity = Math.max(0.4, Math.min(1.5, data.sensitivity)); if (Number.isFinite(data.deadzone)) this.settings.deadzone = Math.max(0.03, Math.min(0.3, data.deadzone)); if (['spring', 'hold'].includes(data.behavior)) this.settings.behavior = data.behavior; if (typeof data.invert === 'boolean') this.settings.invert = data.invert; this.onStatus(this.status()); return this.status(); } tick() { const time = this.clock(), dt = Math.min(0.05, Math.max(0, (time - this.lastTick) / 1000)); this.lastTick = time; if (this.pad && time - this.lastFrame > 2500) { this.pad = null; this.error = 'Controller input stopped'; this.onStatus(this.status()); } if (this.active) { this.pose = padPose(this.pad, this.pose, this.settings, dt); this.onPose(this.pose); const menu = this.pad.buttons[9].pressed; if (menu && !this.menuDown) this.onCamera(); this.menuDown = menu; } else if (this.wasActive && !this.pad) { this.pose = neutral(); this.onPose(this.pose); this.menuDown = false; } this.wasActive = this.active; } start() { if (process.platform !== 'win32') { this.error = 'Windows XInput is unavailable on this system'; return; } this.child = launchXInput(); const lines = createInterface({ input: this.child.stdout }); lines.on('line', (line) => { try { this.accept(JSON.parse(line.replace(/^\uFEFF/, ''))); } catch {} }); const failed = () => { this.available = false; this.pad = null; this.error = 'Windows controller helper stopped'; this.onStatus(this.status()); }; this.child.on('error', failed); this.child.on('exit', failed); this.child.stderr.on('data', () => {}); this.timer = setInterval(() => this.tick(), 16); } stop() { clearInterval(this.timer); this.child?.kill(); }}