fix: ditched sdl and moved to xinput for windows for less ram usage
This commit is contained in:
parent
90d6711935
commit
dc0f2d150a
18 changed files with 663 additions and 100 deletions
|
|
@ -22,7 +22,7 @@ import UpdateStoreJob from "./jobs/update-store";
|
|||
import { getStoreFolder } from "./store/services/gamesService";
|
||||
import { PluginManager } from "./plugins/plugin-manager";
|
||||
import registerPlugins from "./plugins/register-plugins";
|
||||
import controls from '../controls';
|
||||
import controls from './controls/controls';
|
||||
|
||||
export const config = new Conf<SettingsType>({
|
||||
projectName: projectPackage.name,
|
||||
|
|
|
|||
40
src/bun/api/controls/controls.ts
Normal file
40
src/bun/api/controls/controls.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { LaunchGameJob } from '../jobs/launch-game-job';
|
||||
import { events, taskQueue } from '../app';
|
||||
import { GamepadManager } from './manager';
|
||||
|
||||
export default async function Initialize ()
|
||||
{
|
||||
let startSelectPressed = false;
|
||||
|
||||
const manager = new GamepadManager();
|
||||
|
||||
setInterval(() =>
|
||||
{
|
||||
for (const pad of manager.getGamepads())
|
||||
{
|
||||
const state = pad.update();
|
||||
if (!state) continue;
|
||||
|
||||
if (state.buttons.START && state.buttons.SELECT)
|
||||
{
|
||||
if (!startSelectPressed)
|
||||
{
|
||||
startSelectPressed = true;
|
||||
console.log("Focus");
|
||||
const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob);
|
||||
if (launchGameTask)
|
||||
{
|
||||
launchGameTask.abort('exit');
|
||||
taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300));
|
||||
} else
|
||||
{
|
||||
events.emit('focus');
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
startSelectPressed = false;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
32
src/bun/api/controls/gamepad.ts
Normal file
32
src/bun/api/controls/gamepad.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// ./gamepad/index.ts
|
||||
|
||||
import { platform } from "os";
|
||||
import { GamepadWindows } from "./windows";
|
||||
import { GamepadLinux } from "./linux";
|
||||
import type { IGamepadBackend, GamepadState } from "./types";
|
||||
|
||||
export class Gamepad
|
||||
{
|
||||
private backend: IGamepadBackend;
|
||||
|
||||
constructor(index = 0)
|
||||
{
|
||||
if (platform() === "win32")
|
||||
{
|
||||
this.backend = new GamepadWindows(index);
|
||||
} else
|
||||
{
|
||||
this.backend = new GamepadLinux(index);
|
||||
}
|
||||
}
|
||||
|
||||
update (): GamepadState | null
|
||||
{
|
||||
return this.backend.update();
|
||||
}
|
||||
|
||||
close ()
|
||||
{
|
||||
this.backend.close?.();
|
||||
}
|
||||
}
|
||||
87
src/bun/api/controls/linux.ts
Normal file
87
src/bun/api/controls/linux.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
|
||||
import { openSync, readSync, closeSync, readdirSync } from "fs";
|
||||
|
||||
export class GamepadLinux implements IGamepadBackend
|
||||
{
|
||||
private fd: number;
|
||||
private buttons: boolean[];
|
||||
private axes: number[];
|
||||
private buttonsCount = 16;
|
||||
private axesCount = 4;
|
||||
|
||||
constructor(index = 0)
|
||||
{
|
||||
const devices = readdirSync("/dev/input").filter(f => f.startsWith("js"));
|
||||
if (!devices[index]) throw new Error("No gamepad found");
|
||||
const path = `/dev/input/${devices[index]}`;
|
||||
this.fd = openSync(path, "r");
|
||||
|
||||
this.buttons = Array(this.buttonsCount).fill(false);
|
||||
this.axes = Array(this.axesCount).fill(0);
|
||||
}
|
||||
|
||||
update (): GamepadState | null
|
||||
{
|
||||
const buf = Buffer.alloc(8);
|
||||
let bytesRead;
|
||||
try
|
||||
{
|
||||
bytesRead = readSync(this.fd, buf, 0, 8, null);
|
||||
} catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (bytesRead !== 8) return null;
|
||||
|
||||
const [time, value, type, number] = [
|
||||
buf.readUInt32LE(0),
|
||||
buf.readInt16LE(4),
|
||||
buf[6],
|
||||
buf[7],
|
||||
];
|
||||
|
||||
if (type === 1) this.buttons[number] = value !== 0;
|
||||
else if (type === 2 && number < 4) this.axes[number] = value / 32767;
|
||||
|
||||
const btnMap: Record<ButtonName, boolean> = {
|
||||
A: this.buttons[0] ?? false,
|
||||
B: this.buttons[1] ?? false,
|
||||
X: this.buttons[2] ?? false,
|
||||
Y: this.buttons[3] ?? false,
|
||||
UP: this.buttons[4] ?? false,
|
||||
DOWN: this.buttons[5] ?? false,
|
||||
LEFT: this.buttons[6] ?? false,
|
||||
RIGHT: this.buttons[7] ?? false,
|
||||
LB: this.buttons[8] ?? false,
|
||||
RB: this.buttons[9] ?? false,
|
||||
START: this.buttons[10] ?? false,
|
||||
SELECT: this.buttons[11] ?? false,
|
||||
L3: this.buttons[12] ?? false,
|
||||
R3: this.buttons[13] ?? false,
|
||||
};
|
||||
|
||||
return {
|
||||
buttons: btnMap,
|
||||
leftStick: { x: this.axes[0] ?? 0, y: this.axes[1] ?? 0 },
|
||||
rightStick: { x: this.axes[2] ?? 0, y: this.axes[3] ?? 0 },
|
||||
triggers: { left: 0, right: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
isConnected ()
|
||||
{
|
||||
try
|
||||
{
|
||||
readSync(this.fd, Buffer.alloc(1), 0, 1, null);
|
||||
return true;
|
||||
} catch
|
||||
{
|
||||
return false; // file disappeared or read failed
|
||||
}
|
||||
}
|
||||
|
||||
close ()
|
||||
{
|
||||
closeSync(this.fd);
|
||||
}
|
||||
}
|
||||
55
src/bun/api/controls/manager.ts
Normal file
55
src/bun/api/controls/manager.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Gamepad } from "./gamepad";
|
||||
import { platform } from "os";
|
||||
|
||||
export class GamepadManager
|
||||
{
|
||||
private gamepads: Gamepad[] = [];
|
||||
private scanInterval: any;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.scanGamepads();
|
||||
// scan every second for new/disconnected devices
|
||||
this.scanInterval = setInterval(() => this.scanGamepads(), 1000);
|
||||
}
|
||||
|
||||
private scanGamepads ()
|
||||
{
|
||||
const max = platform() === "win32" ? 4 : 8; // max controllers
|
||||
for (let i = 0; i < max; i++)
|
||||
{
|
||||
if (!this.gamepads[i])
|
||||
{
|
||||
try
|
||||
{
|
||||
const pad = new Gamepad(i);
|
||||
if (pad.update())
|
||||
{
|
||||
this.gamepads[i] = pad;
|
||||
console.log(`Gamepad ${i} connected`);
|
||||
}
|
||||
} catch { }
|
||||
} else
|
||||
{
|
||||
const connected = this.gamepads[i].update() !== null;
|
||||
if (!connected)
|
||||
{
|
||||
console.log(`Gamepad ${i} disconnected`);
|
||||
this.gamepads[i].close();
|
||||
delete this.gamepads[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getGamepads ()
|
||||
{
|
||||
return this.gamepads.filter(Boolean);
|
||||
}
|
||||
|
||||
stop ()
|
||||
{
|
||||
clearInterval(this.scanInterval);
|
||||
for (const pad of this.gamepads) pad.close?.();
|
||||
}
|
||||
}
|
||||
38
src/bun/api/controls/types.ts
Normal file
38
src/bun/api/controls/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
export type ButtonName =
|
||||
| "A" | "B" | "X" | "Y"
|
||||
| "UP" | "DOWN" | "LEFT" | "RIGHT"
|
||||
| "LB" | "RB"
|
||||
| "START" | "SELECT"
|
||||
| "L3" | "R3";
|
||||
|
||||
export interface Stick
|
||||
{
|
||||
x: number; // -1 → 1
|
||||
y: number; // -1 → 1
|
||||
}
|
||||
|
||||
export interface Triggers
|
||||
{
|
||||
left: number; // 0 → 1
|
||||
right: number; // 0 → 1
|
||||
}
|
||||
|
||||
export interface GamepadState
|
||||
{
|
||||
buttons: Record<ButtonName, boolean>;
|
||||
leftStick: Stick;
|
||||
rightStick: Stick;
|
||||
triggers: Triggers;
|
||||
}
|
||||
|
||||
export interface IGamepadBackend
|
||||
{
|
||||
/** Polls the current state; returns null if disconnected */
|
||||
update (): GamepadState | null;
|
||||
|
||||
/** Optional: release resources (like closing fd on Linux) */
|
||||
close?(): void;
|
||||
|
||||
/** Optional: check if the gamepad is still connected */
|
||||
isConnected?(): boolean;
|
||||
}
|
||||
57
src/bun/api/controls/windows.ts
Normal file
57
src/bun/api/controls/windows.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
|
||||
import { dlopen, FFIType } from "bun:ffi";
|
||||
|
||||
const xinput = dlopen("xinput1_4.dll", {
|
||||
XInputGetState: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.u32 },
|
||||
});
|
||||
const ERROR_SUCCESS = 0;
|
||||
|
||||
export class GamepadWindows implements IGamepadBackend
|
||||
{
|
||||
private index: number;
|
||||
private buffer = new ArrayBuffer(16);
|
||||
private view = new DataView(this.buffer);
|
||||
private prevButtons = 0;
|
||||
private currButtons = 0;
|
||||
|
||||
constructor(index = 0) { this.index = index; }
|
||||
|
||||
update (): GamepadState | null
|
||||
{
|
||||
const res = xinput.symbols.XInputGetState(this.index, this.buffer);
|
||||
if (res !== ERROR_SUCCESS) return null;
|
||||
|
||||
this.prevButtons = this.currButtons;
|
||||
this.currButtons = this.view.getUint16(4, true);
|
||||
|
||||
const btns: Record<ButtonName, boolean> = {
|
||||
A: (this.currButtons & 0x1000) !== 0,
|
||||
B: (this.currButtons & 0x2000) !== 0,
|
||||
X: (this.currButtons & 0x4000) !== 0,
|
||||
Y: (this.currButtons & 0x8000) !== 0,
|
||||
UP: (this.currButtons & 0x0001) !== 0,
|
||||
DOWN: (this.currButtons & 0x0002) !== 0,
|
||||
LEFT: (this.currButtons & 0x0004) !== 0,
|
||||
RIGHT: (this.currButtons & 0x0008) !== 0,
|
||||
LB: (this.currButtons & 0x0100) !== 0,
|
||||
RB: (this.currButtons & 0x0200) !== 0,
|
||||
START: (this.currButtons & 0x0010) !== 0,
|
||||
SELECT: (this.currButtons & 0x0020) !== 0,
|
||||
L3: (this.currButtons & 0x0040) !== 0,
|
||||
R3: (this.currButtons & 0x0080) !== 0,
|
||||
};
|
||||
|
||||
return {
|
||||
buttons: btns,
|
||||
leftStick: { x: this.view.getInt16(6, true) / 32767, y: this.view.getInt16(8, true) / 32767 },
|
||||
rightStick: { x: this.view.getInt16(10, true) / 32767, y: this.view.getInt16(12, true) / 32767 },
|
||||
triggers: { left: this.view.getUint8(14) / 255, right: this.view.getUint8(15) / 255 },
|
||||
};
|
||||
}
|
||||
|
||||
isConnected ()
|
||||
{
|
||||
const res = xinput.symbols.XInputGetState(this.index, this.buffer);
|
||||
return res === ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue