emoemu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import HID from 'node-hid';
|
|
2
|
+
import { pipe, filter, map } from 'remeda';
|
|
3
|
+
import { safeClose } from '../../utils/safeClose';
|
|
4
|
+
import { StandardButton } from '../../core/button';
|
|
5
|
+
import { createOppositeDirections } from '../inputUtils';
|
|
6
|
+
|
|
7
|
+
export * from './consts';
|
|
8
|
+
|
|
9
|
+
import { ALL_STANDARD_BUTTONS } from './consts';
|
|
10
|
+
import {
|
|
11
|
+
GamepadProfile,
|
|
12
|
+
AnalogState,
|
|
13
|
+
findProfile,
|
|
14
|
+
isGamepadDevice,
|
|
15
|
+
gamepadProfiles,
|
|
16
|
+
} from '../gamepadProfiles';
|
|
17
|
+
import {
|
|
18
|
+
notifyGamepadConnected,
|
|
19
|
+
notifyGamepadDisconnected,
|
|
20
|
+
} from '../../frontend/notifications';
|
|
21
|
+
import {
|
|
22
|
+
GAMEPAD_SCAN_INTERVAL_MS,
|
|
23
|
+
MAX_GAMEPADS,
|
|
24
|
+
HEX_BASE,
|
|
25
|
+
PROFILE_NAME_DISPLAY_LENGTH,
|
|
26
|
+
ANALOG_DEBUG_DECIMALS,
|
|
27
|
+
} from '..';
|
|
28
|
+
import { logger } from '../../utils/logger';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Callback type for button state changes
|
|
32
|
+
*/
|
|
33
|
+
export type GamepadButtonCallback = (
|
|
34
|
+
port: number,
|
|
35
|
+
button: StandardButton,
|
|
36
|
+
pressed: boolean
|
|
37
|
+
) => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Callback type for analog axis changes
|
|
41
|
+
* @param port Controller port (0-based)
|
|
42
|
+
* @param index Analog stick (0=left, 1=right)
|
|
43
|
+
* @param axis Axis (0=X, 1=Y)
|
|
44
|
+
* @param value Normalized value from -1.0 to 1.0
|
|
45
|
+
*/
|
|
46
|
+
export type GamepadAnalogCallback = (
|
|
47
|
+
port: number,
|
|
48
|
+
index: number,
|
|
49
|
+
axis: number,
|
|
50
|
+
value: number
|
|
51
|
+
) => void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Represents a connected gamepad device
|
|
55
|
+
*/
|
|
56
|
+
interface ConnectedGamepad {
|
|
57
|
+
device: HID.HID;
|
|
58
|
+
profile: GamepadProfile;
|
|
59
|
+
deviceInfo: HID.Device;
|
|
60
|
+
controllerPort: 0 | 1;
|
|
61
|
+
lastButtonState: Map<StandardButton, boolean>;
|
|
62
|
+
lastAnalogState: AnalogState | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Manages gamepad input via HID devices
|
|
68
|
+
* Supports Xbox, PlayStation, Nintendo, and generic USB gamepads
|
|
69
|
+
*/
|
|
70
|
+
export class GamepadManager {
|
|
71
|
+
private gamepads: ConnectedGamepad[] = [];
|
|
72
|
+
private scanInterval: ReturnType<typeof setInterval> | null = null;
|
|
73
|
+
private enabled: boolean = false;
|
|
74
|
+
private initialScanComplete: boolean = false;
|
|
75
|
+
|
|
76
|
+
/** Callback when button state changes */
|
|
77
|
+
public onButtonChange: GamepadButtonCallback | null = null;
|
|
78
|
+
|
|
79
|
+
/** Callback when analog axis changes */
|
|
80
|
+
public onAnalogChange: GamepadAnalogCallback | null = null;
|
|
81
|
+
|
|
82
|
+
constructor() {}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start the gamepad manager
|
|
86
|
+
* Scans for devices and begins reading input
|
|
87
|
+
*/
|
|
88
|
+
start(): void {
|
|
89
|
+
if (this.enabled) {return;}
|
|
90
|
+
this.enabled = true;
|
|
91
|
+
|
|
92
|
+
// Log joypad driver (RetroArch-style)
|
|
93
|
+
logger.info('Found joypad driver: "hid"', 'Joypad');
|
|
94
|
+
|
|
95
|
+
// Initial device scan (silent - no notifications)
|
|
96
|
+
this.scanForDevices();
|
|
97
|
+
this.initialScanComplete = true;
|
|
98
|
+
|
|
99
|
+
// Periodically scan for new devices (hotplug support)
|
|
100
|
+
this.scanInterval = setInterval(() => {
|
|
101
|
+
this.scanForDevices();
|
|
102
|
+
}, GAMEPAD_SCAN_INTERVAL_MS);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stop the gamepad manager and release all devices
|
|
107
|
+
*/
|
|
108
|
+
stop(): void {
|
|
109
|
+
this.enabled = false;
|
|
110
|
+
|
|
111
|
+
if (this.scanInterval) {
|
|
112
|
+
clearInterval(this.scanInterval);
|
|
113
|
+
this.scanInterval = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Close all connected gamepads
|
|
117
|
+
for (const gamepad of this.gamepads) {
|
|
118
|
+
safeClose(gamepad.device);
|
|
119
|
+
}
|
|
120
|
+
this.gamepads = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Scan for and connect to gamepad devices
|
|
125
|
+
*/
|
|
126
|
+
private scanForDevices(): void {
|
|
127
|
+
if (!this.enabled) {return;}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const devices = HID.devices();
|
|
131
|
+
const gamepadDevices = devices.filter(isGamepadDevice);
|
|
132
|
+
|
|
133
|
+
for (const deviceInfo of gamepadDevices) {
|
|
134
|
+
// Skip if already connected
|
|
135
|
+
if (this.isDeviceConnected(deviceInfo)) {continue;}
|
|
136
|
+
|
|
137
|
+
// Skip if we already have max gamepads
|
|
138
|
+
if (this.gamepads.length >= MAX_GAMEPADS) {continue;}
|
|
139
|
+
|
|
140
|
+
// Try to connect
|
|
141
|
+
this.connectDevice(deviceInfo);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// HID enumeration can fail on some systems - ignore silently
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a device is already connected
|
|
150
|
+
*/
|
|
151
|
+
private isDeviceConnected(deviceInfo: HID.Device): boolean {
|
|
152
|
+
return this.gamepads.some(
|
|
153
|
+
(gp) =>
|
|
154
|
+
gp.deviceInfo.vendorId === deviceInfo.vendorId &&
|
|
155
|
+
gp.deviceInfo.productId === deviceInfo.productId &&
|
|
156
|
+
gp.deviceInfo.path === deviceInfo.path
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Attempt to connect to a gamepad device
|
|
162
|
+
*/
|
|
163
|
+
private connectDevice(deviceInfo: HID.Device): void {
|
|
164
|
+
if (!deviceInfo.path) {return;}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const device = new HID.HID(deviceInfo.path);
|
|
168
|
+
const profile = findProfile(
|
|
169
|
+
deviceInfo.vendorId,
|
|
170
|
+
deviceInfo.productId
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Assign to next available controller port (0-indexed for core compatibility)
|
|
174
|
+
const controllerPort: 0 | 1 = this.gamepads.length === 0 ? 0 : 1;
|
|
175
|
+
|
|
176
|
+
const gamepad: ConnectedGamepad = {
|
|
177
|
+
device,
|
|
178
|
+
profile,
|
|
179
|
+
deviceInfo,
|
|
180
|
+
controllerPort,
|
|
181
|
+
lastButtonState: new Map(),
|
|
182
|
+
lastAnalogState: null,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Set up data handler
|
|
186
|
+
device.on('data', (data: Buffer) => {
|
|
187
|
+
this.handleInput(gamepad, data);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Handle disconnection
|
|
191
|
+
device.on('error', () => {
|
|
192
|
+
this.disconnectGamepad(gamepad);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.gamepads.push(gamepad);
|
|
196
|
+
|
|
197
|
+
// Log gamepad connection (RetroArch-style)
|
|
198
|
+
logger.info(`${profile.name} configured in port ${controllerPort + 1}`, 'Autoconf');
|
|
199
|
+
logger.debug(`Joypad connected: ${deviceInfo.product ?? 'Unknown'} (VID=${deviceInfo.vendorId.toString(HEX_BASE)}, PID=${deviceInfo.productId.toString(HEX_BASE)})`, 'Joypad');
|
|
200
|
+
|
|
201
|
+
// Only notify for hotplugged gamepads, not ones already connected at startup
|
|
202
|
+
if (this.initialScanComplete) {
|
|
203
|
+
notifyGamepadConnected(profile.name, controllerPort + 1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
logger.debug(
|
|
207
|
+
`Gamepad connected: ${profile.name} (${deviceInfo.product ?? 'Unknown'}) -> Player ${controllerPort + 1}`,
|
|
208
|
+
'Joypad'
|
|
209
|
+
);
|
|
210
|
+
} catch {
|
|
211
|
+
// Failed to open device - might be in use or require permissions
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Deadzone threshold for analog stick changes (1%) */
|
|
216
|
+
private static readonly ANALOG_DEADZONE = 0.01;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle input data from a gamepad
|
|
220
|
+
*/
|
|
221
|
+
private handleInput(gamepad: ConnectedGamepad, data: Buffer): void {
|
|
222
|
+
logger.debug(
|
|
223
|
+
`[${gamepad.profile.name}] Raw: ${Array.from(data)
|
|
224
|
+
.map((b) => b.toString(HEX_BASE).padStart(2, '0'))
|
|
225
|
+
.join(' ')}`,
|
|
226
|
+
'Joypad'
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const buttonStates = gamepad.profile.parseReport(data);
|
|
231
|
+
|
|
232
|
+
// Update all tracked buttons
|
|
233
|
+
for (const button of ALL_STANDARD_BUTTONS) {
|
|
234
|
+
const pressed = buttonStates.get(button) ?? false;
|
|
235
|
+
const wasPressed = gamepad.lastButtonState.get(button) ?? false;
|
|
236
|
+
|
|
237
|
+
// Only update if state changed
|
|
238
|
+
if (pressed !== wasPressed) {
|
|
239
|
+
// Handle opposite directions - don't allow Up+Down or Left+Right
|
|
240
|
+
if (pressed) {
|
|
241
|
+
this.handleButtonPress(button, gamepad);
|
|
242
|
+
} else {
|
|
243
|
+
// Fire callback for button release
|
|
244
|
+
this.onButtonChange?.(gamepad.controllerPort, button, false);
|
|
245
|
+
}
|
|
246
|
+
gamepad.lastButtonState.set(button, pressed);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Handle analog stick input if profile supports it
|
|
251
|
+
if (gamepad.profile.parseAnalog && this.onAnalogChange) {
|
|
252
|
+
const analogState = gamepad.profile.parseAnalog(data);
|
|
253
|
+
if (analogState) {
|
|
254
|
+
this.handleAnalogInput(gamepad, analogState);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Parsing failed - might be unexpected report format
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Handle analog stick input changes
|
|
264
|
+
*/
|
|
265
|
+
private handleAnalogInput(gamepad: ConnectedGamepad, state: AnalogState): void {
|
|
266
|
+
const last = gamepad.lastAnalogState;
|
|
267
|
+
const port = gamepad.controllerPort;
|
|
268
|
+
const dz = GamepadManager.ANALOG_DEADZONE;
|
|
269
|
+
|
|
270
|
+
// Debug: Log raw analog values
|
|
271
|
+
logger.debug(
|
|
272
|
+
`Analog raw: L(${state.leftX.toFixed(ANALOG_DEBUG_DECIMALS)}, ${state.leftY.toFixed(ANALOG_DEBUG_DECIMALS)}) R(${state.rightX.toFixed(ANALOG_DEBUG_DECIMALS)}, ${state.rightY.toFixed(ANALOG_DEBUG_DECIMALS)})`,
|
|
273
|
+
'Joypad'
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Left stick X (index=0, axis=0)
|
|
277
|
+
if (!last || Math.abs(state.leftX - last.leftX) > dz) {
|
|
278
|
+
this.onAnalogChange?.(port, 0, 0, state.leftX);
|
|
279
|
+
}
|
|
280
|
+
// Left stick Y (index=0, axis=1)
|
|
281
|
+
if (!last || Math.abs(state.leftY - last.leftY) > dz) {
|
|
282
|
+
this.onAnalogChange?.(port, 0, 1, state.leftY);
|
|
283
|
+
}
|
|
284
|
+
// Right stick X (index=1, axis=0)
|
|
285
|
+
if (!last || Math.abs(state.rightX - last.rightX) > dz) {
|
|
286
|
+
this.onAnalogChange?.(port, 1, 0, state.rightX);
|
|
287
|
+
}
|
|
288
|
+
// Right stick Y (index=1, axis=1)
|
|
289
|
+
if (!last || Math.abs(state.rightY - last.rightY) > dz) {
|
|
290
|
+
this.onAnalogChange?.(port, 1, 1, state.rightY);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
gamepad.lastAnalogState = state;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Opposite D-pad directions for preventing simultaneous Up+Down or Left+Right
|
|
298
|
+
*/
|
|
299
|
+
private static readonly OPPOSITE_DIRECTIONS: Map<StandardButton, StandardButton> = createOppositeDirections(
|
|
300
|
+
StandardButton.Up,
|
|
301
|
+
StandardButton.Down,
|
|
302
|
+
StandardButton.Left,
|
|
303
|
+
StandardButton.Right
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handle button press with opposite direction logic
|
|
308
|
+
*/
|
|
309
|
+
private handleButtonPress(
|
|
310
|
+
button: StandardButton,
|
|
311
|
+
gamepad: ConnectedGamepad
|
|
312
|
+
): void {
|
|
313
|
+
// Release opposite direction if pressing a direction
|
|
314
|
+
const opposite = GamepadManager.OPPOSITE_DIRECTIONS.get(button);
|
|
315
|
+
if (opposite !== undefined && gamepad.lastButtonState.get(opposite)) {
|
|
316
|
+
// Release opposite direction first
|
|
317
|
+
this.onButtonChange?.(gamepad.controllerPort, opposite, false);
|
|
318
|
+
gamepad.lastButtonState.set(opposite, false);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Fire callback for button press
|
|
322
|
+
this.onButtonChange?.(gamepad.controllerPort, button, true);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Disconnect a gamepad
|
|
327
|
+
*/
|
|
328
|
+
private disconnectGamepad(gamepad: ConnectedGamepad): void {
|
|
329
|
+
const index = this.gamepads.indexOf(gamepad);
|
|
330
|
+
if (index === -1) {return;}
|
|
331
|
+
|
|
332
|
+
safeClose(gamepad.device);
|
|
333
|
+
|
|
334
|
+
this.gamepads.splice(index, 1);
|
|
335
|
+
notifyGamepadDisconnected(gamepad.profile.name, gamepad.controllerPort + 1);
|
|
336
|
+
|
|
337
|
+
// Release all buttons on the controller
|
|
338
|
+
for (const button of ALL_STANDARD_BUTTONS) {
|
|
339
|
+
if (gamepad.lastButtonState.get(button)) {
|
|
340
|
+
this.onButtonChange?.(gamepad.controllerPort, button, false);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
logger.debug(
|
|
345
|
+
`Gamepad disconnected: ${gamepad.profile.name} (Player ${gamepad.controllerPort + 1})`,
|
|
346
|
+
'Joypad'
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get number of connected gamepads
|
|
352
|
+
*/
|
|
353
|
+
getConnectedCount(): number {
|
|
354
|
+
return this.gamepads.length;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get debug info about connected gamepads
|
|
359
|
+
*/
|
|
360
|
+
getDebugInfo(): string {
|
|
361
|
+
if (this.gamepads.length === 0) {
|
|
362
|
+
return 'No gamepads';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return pipe(
|
|
366
|
+
this.gamepads,
|
|
367
|
+
map((gp) => `P${gp.controllerPort + 1}: ${gp.profile.name.substring(0, PROFILE_NAME_DISPLAY_LENGTH)}`)
|
|
368
|
+
).join(', ');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get short status string for player 1's input device
|
|
373
|
+
*/
|
|
374
|
+
getPlayer1Status(): string | null {
|
|
375
|
+
const p1 = this.gamepads.find((gp) => gp.controllerPort === 0);
|
|
376
|
+
return p1 ? p1.profile.name : null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* List all detected gamepad devices (for diagnostics)
|
|
381
|
+
*/
|
|
382
|
+
static listDevices(): Array<{
|
|
383
|
+
vendorId: number;
|
|
384
|
+
productId: number;
|
|
385
|
+
product: string;
|
|
386
|
+
manufacturer: string;
|
|
387
|
+
profile: string;
|
|
388
|
+
path: string;
|
|
389
|
+
}> {
|
|
390
|
+
try {
|
|
391
|
+
return pipe(
|
|
392
|
+
HID.devices(),
|
|
393
|
+
filter(isGamepadDevice),
|
|
394
|
+
map((d) => ({
|
|
395
|
+
vendorId: d.vendorId,
|
|
396
|
+
productId: d.productId,
|
|
397
|
+
product: d.product ?? 'Unknown',
|
|
398
|
+
manufacturer: d.manufacturer ?? 'Unknown',
|
|
399
|
+
profile: findProfile(d.vendorId, d.productId).name,
|
|
400
|
+
path: d.path ?? '',
|
|
401
|
+
}))
|
|
402
|
+
);
|
|
403
|
+
} catch {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get list of supported controller profiles
|
|
410
|
+
*/
|
|
411
|
+
static getSupportedProfiles(): string[] {
|
|
412
|
+
return pipe(
|
|
413
|
+
gamepadProfiles,
|
|
414
|
+
filter((p) => p.vendorIds.length > 0),
|
|
415
|
+
map((p) => p.name)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Button } from '../Controller';
|
|
2
|
+
import { createOppositeDirections } from '../inputUtils';
|
|
3
|
+
import {
|
|
4
|
+
KITTY_KEY_ARROW_UP,
|
|
5
|
+
KITTY_KEY_ARROW_DOWN,
|
|
6
|
+
KITTY_KEY_ARROW_LEFT,
|
|
7
|
+
KITTY_KEY_ARROW_RIGHT,
|
|
8
|
+
KEY_CODE_W,
|
|
9
|
+
KEY_CODE_S,
|
|
10
|
+
KEY_CODE_A,
|
|
11
|
+
KEY_CODE_D,
|
|
12
|
+
KEY_CODE_K,
|
|
13
|
+
KEY_CODE_Z,
|
|
14
|
+
KEY_CODE_J,
|
|
15
|
+
KEY_CODE_X,
|
|
16
|
+
KEY_CODE_ENTER,
|
|
17
|
+
KEY_CODE_SPACE,
|
|
18
|
+
} from '..';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Kitty keyboard protocol key codes (Unicode codepoints)
|
|
22
|
+
* https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
23
|
+
*/
|
|
24
|
+
export const KITTY_KEY_TO_BUTTON: Map<number, Button> = new Map([
|
|
25
|
+
// WASD for D-Pad (lowercase)
|
|
26
|
+
[KEY_CODE_W, Button.Up],
|
|
27
|
+
[KEY_CODE_S, Button.Down],
|
|
28
|
+
[KEY_CODE_A, Button.Left],
|
|
29
|
+
[KEY_CODE_D, Button.Right],
|
|
30
|
+
|
|
31
|
+
// Action buttons
|
|
32
|
+
[KEY_CODE_K, Button.A],
|
|
33
|
+
[KEY_CODE_Z, Button.A],
|
|
34
|
+
[KEY_CODE_J, Button.B],
|
|
35
|
+
[KEY_CODE_X, Button.B],
|
|
36
|
+
|
|
37
|
+
// Start/Select
|
|
38
|
+
[KEY_CODE_ENTER, Button.Start],
|
|
39
|
+
[KEY_CODE_SPACE, Button.Select],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// Special keys use different codes in Kitty protocol
|
|
43
|
+
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
|
|
44
|
+
export const KITTY_SPECIAL_KEYS: Map<number, Button> = new Map([
|
|
45
|
+
[KITTY_KEY_ARROW_UP, Button.Up],
|
|
46
|
+
[KITTY_KEY_ARROW_DOWN, Button.Down],
|
|
47
|
+
[KITTY_KEY_ARROW_LEFT, Button.Left],
|
|
48
|
+
[KITTY_KEY_ARROW_RIGHT, Button.Right],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Legacy key mappings for non-Kitty terminals
|
|
53
|
+
*/
|
|
54
|
+
export const LEGACY_KEY_TO_BUTTON: Map<string, Button> = new Map([
|
|
55
|
+
// WASD
|
|
56
|
+
['w', Button.Up], ['W', Button.Up],
|
|
57
|
+
['s', Button.Down], ['S', Button.Down],
|
|
58
|
+
['a', Button.Left], ['A', Button.Left],
|
|
59
|
+
['d', Button.Right], ['D', Button.Right],
|
|
60
|
+
|
|
61
|
+
// Arrow keys (legacy escape sequences)
|
|
62
|
+
['\x1b[A', Button.Up],
|
|
63
|
+
['\x1b[B', Button.Down],
|
|
64
|
+
['\x1b[C', Button.Right],
|
|
65
|
+
['\x1b[D', Button.Left],
|
|
66
|
+
|
|
67
|
+
// Action buttons
|
|
68
|
+
['k', Button.A], ['K', Button.A],
|
|
69
|
+
['z', Button.A], ['Z', Button.A],
|
|
70
|
+
['j', Button.B], ['J', Button.B],
|
|
71
|
+
['x', Button.B], ['X', Button.B],
|
|
72
|
+
|
|
73
|
+
// Start/Select
|
|
74
|
+
['\r', Button.Start],
|
|
75
|
+
[' ', Button.Select],
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* D-pad buttons that are mutually exclusive (opposite directions)
|
|
80
|
+
*/
|
|
81
|
+
export const OPPOSITE_DIRECTIONS: Map<Button, Button> = createOppositeDirections(
|
|
82
|
+
Button.Up,
|
|
83
|
+
Button.Down,
|
|
84
|
+
Button.Left,
|
|
85
|
+
Button.Right
|
|
86
|
+
);
|