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,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Mapper
|
|
3
|
+
*
|
|
4
|
+
* Translates physical inputs (keyboard keys, gamepad buttons) to core-specific
|
|
5
|
+
* button IDs. This allows the same physical controls to work across different
|
|
6
|
+
* emulated systems.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Frontend receives keyboard/gamepad input
|
|
10
|
+
* 2. Input is translated to StandardButton enum
|
|
11
|
+
* 3. StandardButton is mapped to core-specific button ID via button name matching
|
|
12
|
+
* 4. Core receives setButtonState(port, coreButtonId, pressed)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { pipe, map, filter, isNonNull } from 'remeda';
|
|
16
|
+
import type { ButtonDefinition } from '../../core/core';
|
|
17
|
+
import {
|
|
18
|
+
StandardButton,
|
|
19
|
+
DEFAULT_KEYBOARD_MAP,
|
|
20
|
+
areOppositeDirections,
|
|
21
|
+
} from '../../core/button';
|
|
22
|
+
import { logger } from '../../utils/logger';
|
|
23
|
+
|
|
24
|
+
export * from './consts';
|
|
25
|
+
|
|
26
|
+
import { BUTTON_NAME_MAP, ANALOG_INDEX, ANALOG_AXIS } from './consts';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Callback type for button state changes
|
|
30
|
+
*/
|
|
31
|
+
export type ButtonChangeCallback = (
|
|
32
|
+
port: number,
|
|
33
|
+
button: number,
|
|
34
|
+
pressed: boolean
|
|
35
|
+
) => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Callback type for analog axis changes
|
|
39
|
+
* @param port Controller port (0-based)
|
|
40
|
+
* @param index Analog stick (0=left, 1=right)
|
|
41
|
+
* @param axis Axis (0=X, 1=Y)
|
|
42
|
+
* @param value Normalized value from -1.0 to 1.0
|
|
43
|
+
*/
|
|
44
|
+
export type AnalogChangeCallback = (
|
|
45
|
+
port: number,
|
|
46
|
+
index: number,
|
|
47
|
+
axis: number,
|
|
48
|
+
value: number
|
|
49
|
+
) => void;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maps physical inputs to core-specific buttons
|
|
54
|
+
*/
|
|
55
|
+
export class InputMapper {
|
|
56
|
+
/** Core's button definitions */
|
|
57
|
+
private coreButtons: ButtonDefinition[];
|
|
58
|
+
|
|
59
|
+
/** Map from StandardButton to core button ID */
|
|
60
|
+
private standardToCore: Map<StandardButton, number>;
|
|
61
|
+
|
|
62
|
+
/** Map from keyboard key to StandardButton */
|
|
63
|
+
private keyboardMap: Map<string, StandardButton>;
|
|
64
|
+
|
|
65
|
+
/** Current button state per port: port -> (coreButtonId -> pressed) */
|
|
66
|
+
private portState: Map<number, Map<number, boolean>>;
|
|
67
|
+
|
|
68
|
+
/** Analog axis state per port: port -> index -> axis -> value */
|
|
69
|
+
private analogState: Map<number, Map<number, Map<number, number>>>;
|
|
70
|
+
|
|
71
|
+
/** Callback when button state changes */
|
|
72
|
+
public onButtonChange: ButtonChangeCallback | null = null;
|
|
73
|
+
|
|
74
|
+
/** Callback when analog axis state changes */
|
|
75
|
+
public onAnalogChange: AnalogChangeCallback | null = null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create an input mapper for a specific core's buttons.
|
|
79
|
+
*
|
|
80
|
+
* @param coreButtons Button definitions from SystemInfo.buttons
|
|
81
|
+
* @param maxPlayers Maximum number of controller ports
|
|
82
|
+
*/
|
|
83
|
+
constructor(coreButtons: ButtonDefinition[], maxPlayers: number = 2) {
|
|
84
|
+
this.coreButtons = coreButtons;
|
|
85
|
+
this.keyboardMap = new Map(DEFAULT_KEYBOARD_MAP);
|
|
86
|
+
|
|
87
|
+
// Initialize port state
|
|
88
|
+
this.portState = new Map();
|
|
89
|
+
this.analogState = new Map();
|
|
90
|
+
for (let port = 0; port < maxPlayers; port++) {
|
|
91
|
+
this.portState.set(port, new Map());
|
|
92
|
+
this.analogState.set(port, new Map());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build mapping from StandardButton to core button IDs by name matching
|
|
96
|
+
this.standardToCore = this.buildDefaultMapping();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build default mapping from StandardButton to core buttons by matching names.
|
|
101
|
+
* This allows automatic mapping without explicit configuration.
|
|
102
|
+
*/
|
|
103
|
+
private buildDefaultMapping(): Map<StandardButton, number> {
|
|
104
|
+
return new Map(
|
|
105
|
+
pipe(
|
|
106
|
+
this.coreButtons,
|
|
107
|
+
map((button) => {
|
|
108
|
+
const name = button.name.toLowerCase();
|
|
109
|
+
const match = BUTTON_NAME_MAP.find((m) => m.names.includes(name));
|
|
110
|
+
return match ? ([match.button, button.id] as const) : null;
|
|
111
|
+
}),
|
|
112
|
+
filter(isNonNull)
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle a keyboard key event.
|
|
119
|
+
*
|
|
120
|
+
* @param key The key that was pressed/released (e.g., 'a', 'Enter', 'ArrowUp')
|
|
121
|
+
* @param pressed Whether the key is pressed (true) or released (false)
|
|
122
|
+
* @param port Controller port (default 0)
|
|
123
|
+
*/
|
|
124
|
+
handleKey(key: string, pressed: boolean, port: number = 0): void {
|
|
125
|
+
const standardButton = this.keyboardMap.get(key);
|
|
126
|
+
if (standardButton === undefined) {return;}
|
|
127
|
+
|
|
128
|
+
this.handleStandardButton(standardButton, pressed, port);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** D-pad to analog axis mapping for keyboard-to-analog conversion */
|
|
132
|
+
private static readonly DIRECTION_TO_ANALOG: ReadonlyMap<
|
|
133
|
+
StandardButton,
|
|
134
|
+
{ axis: number; value: number }
|
|
135
|
+
> = new Map([
|
|
136
|
+
// Left stick: index=0, axis=0 (X), axis=1 (Y)
|
|
137
|
+
// X: negative = left, positive = right
|
|
138
|
+
// Y: negative = up, positive = down
|
|
139
|
+
[StandardButton.Up, { axis: ANALOG_AXIS.Y, value: -1 }],
|
|
140
|
+
[StandardButton.Down, { axis: ANALOG_AXIS.Y, value: 1 }],
|
|
141
|
+
[StandardButton.Left, { axis: ANALOG_AXIS.X, value: -1 }],
|
|
142
|
+
[StandardButton.Right, { axis: ANALOG_AXIS.X, value: 1 }],
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
/** Track keyboard-driven analog state to handle opposite directions */
|
|
146
|
+
private keyboardAnalogState: Map<number, Map<number, number>> = new Map();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle a standard button event (from keyboard or gamepad).
|
|
150
|
+
*
|
|
151
|
+
* @param standardButton The standard button
|
|
152
|
+
* @param pressed Whether pressed or released
|
|
153
|
+
* @param port Controller port
|
|
154
|
+
*/
|
|
155
|
+
handleStandardButton(
|
|
156
|
+
standardButton: StandardButton,
|
|
157
|
+
pressed: boolean,
|
|
158
|
+
port: number = 0
|
|
159
|
+
): void {
|
|
160
|
+
const coreButton = this.standardToCore.get(standardButton);
|
|
161
|
+
if (coreButton === undefined) {return;}
|
|
162
|
+
|
|
163
|
+
// Handle opposite direction prevention for D-pad
|
|
164
|
+
if (pressed) {
|
|
165
|
+
const portState = this.portState.get(port);
|
|
166
|
+
if (portState) {
|
|
167
|
+
// Check if opposite direction is pressed
|
|
168
|
+
for (const [otherStandard, otherCore] of this.standardToCore) {
|
|
169
|
+
if (
|
|
170
|
+
areOppositeDirections(standardButton, otherStandard) &&
|
|
171
|
+
portState.get(otherCore)
|
|
172
|
+
) {
|
|
173
|
+
// Release opposite direction first
|
|
174
|
+
this.setButton(port, otherCore, false);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.setButton(port, coreButton, pressed);
|
|
181
|
+
|
|
182
|
+
// Also send analog input for direction buttons (for systems like N64)
|
|
183
|
+
this.handleDirectionAsAnalog(standardButton, pressed, port);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Convert direction button to analog stick input.
|
|
188
|
+
* This allows keyboard arrows to control analog sticks (useful for N64).
|
|
189
|
+
*/
|
|
190
|
+
private handleDirectionAsAnalog(
|
|
191
|
+
standardButton: StandardButton,
|
|
192
|
+
pressed: boolean,
|
|
193
|
+
port: number
|
|
194
|
+
): void {
|
|
195
|
+
const analogMapping = InputMapper.DIRECTION_TO_ANALOG.get(standardButton);
|
|
196
|
+
if (!analogMapping) {return;}
|
|
197
|
+
|
|
198
|
+
// Initialize keyboard analog tracking for this port if needed
|
|
199
|
+
if (!this.keyboardAnalogState.has(port)) {
|
|
200
|
+
this.keyboardAnalogState.set(port, new Map([[ANALOG_AXIS.X, 0], [ANALOG_AXIS.Y, 0]]));
|
|
201
|
+
}
|
|
202
|
+
const portAnalog = this.keyboardAnalogState.get(port)!;
|
|
203
|
+
|
|
204
|
+
// Calculate new axis value
|
|
205
|
+
const currentValue = portAnalog.get(analogMapping.axis) ?? 0;
|
|
206
|
+
let newValue: number;
|
|
207
|
+
|
|
208
|
+
if (pressed) {
|
|
209
|
+
// Set axis to direction value
|
|
210
|
+
newValue = analogMapping.value;
|
|
211
|
+
} else {
|
|
212
|
+
// On release, check if opposite direction is still held
|
|
213
|
+
// If so, revert to that direction; otherwise, center the axis
|
|
214
|
+
const oppositeButton = this.getOppositeDirection(standardButton);
|
|
215
|
+
const oppositeHeld = oppositeButton !== undefined &&
|
|
216
|
+
this.portState.get(port)?.get(this.standardToCore.get(oppositeButton)!) === true;
|
|
217
|
+
|
|
218
|
+
if (oppositeHeld) {
|
|
219
|
+
// Opposite direction is held, set to its value
|
|
220
|
+
const oppositeMapping = InputMapper.DIRECTION_TO_ANALOG.get(oppositeButton);
|
|
221
|
+
newValue = oppositeMapping?.value ?? 0;
|
|
222
|
+
} else {
|
|
223
|
+
// No direction held, center the axis
|
|
224
|
+
newValue = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Update tracking and fire callback if value changed
|
|
229
|
+
if (newValue !== currentValue) {
|
|
230
|
+
portAnalog.set(analogMapping.axis, newValue);
|
|
231
|
+
// Debug: Log keyboard-to-analog conversion
|
|
232
|
+
logger.debug(`Keyboard analog: button=${StandardButton[standardButton]} axis=${analogMapping.axis} value=${newValue}`, 'Input');
|
|
233
|
+
this.onAnalogChange?.(port, ANALOG_INDEX.LEFT, analogMapping.axis, newValue);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the opposite direction button for a given direction.
|
|
239
|
+
*/
|
|
240
|
+
private getOppositeDirection(button: StandardButton): StandardButton | undefined {
|
|
241
|
+
switch (button) {
|
|
242
|
+
case StandardButton.Up: return StandardButton.Down;
|
|
243
|
+
case StandardButton.Down: return StandardButton.Up;
|
|
244
|
+
case StandardButton.Left: return StandardButton.Right;
|
|
245
|
+
case StandardButton.Right: return StandardButton.Left;
|
|
246
|
+
default: return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handle gamepad button by index (from HID report).
|
|
252
|
+
* This maps the gamepad button index to a StandardButton.
|
|
253
|
+
*
|
|
254
|
+
* @param gamepadButton Gamepad button index (system-specific)
|
|
255
|
+
* @param pressed Whether pressed or released
|
|
256
|
+
* @param port Controller port
|
|
257
|
+
*/
|
|
258
|
+
handleGamepadButton(
|
|
259
|
+
gamepadButton: StandardButton,
|
|
260
|
+
pressed: boolean,
|
|
261
|
+
port: number = 0
|
|
262
|
+
): void {
|
|
263
|
+
this.handleStandardButton(gamepadButton, pressed, port);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Handle analog stick axis input.
|
|
268
|
+
*
|
|
269
|
+
* @param index Analog stick (0=left, 1=right from ANALOG_INDEX)
|
|
270
|
+
* @param axis Axis (0=X, 1=Y from ANALOG_AXIS)
|
|
271
|
+
* @param value Normalized value from -1.0 to 1.0
|
|
272
|
+
* @param port Controller port (default 0)
|
|
273
|
+
*/
|
|
274
|
+
handleAnalogAxis(
|
|
275
|
+
index: number,
|
|
276
|
+
axis: number,
|
|
277
|
+
value: number,
|
|
278
|
+
port: number = 0
|
|
279
|
+
): void {
|
|
280
|
+
const portState = this.analogState.get(port);
|
|
281
|
+
if (!portState) {return;}
|
|
282
|
+
|
|
283
|
+
// Initialize stick state if needed
|
|
284
|
+
if (!portState.has(index)) {
|
|
285
|
+
portState.set(index, new Map([[ANALOG_AXIS.X, 0], [ANALOG_AXIS.Y, 0]]));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const stickState = portState.get(index);
|
|
289
|
+
if (!stickState) {return;}
|
|
290
|
+
|
|
291
|
+
// Only update if value changed significantly (avoid noise)
|
|
292
|
+
const oldValue = stickState.get(axis) ?? 0;
|
|
293
|
+
const DEADZONE = 0.01; // 1% deadzone for noise filtering
|
|
294
|
+
if (Math.abs(value - oldValue) > DEADZONE) {
|
|
295
|
+
stickState.set(axis, value);
|
|
296
|
+
this.onAnalogChange?.(port, index, axis, value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get the analog state for a port.
|
|
302
|
+
*
|
|
303
|
+
* @param port Controller port
|
|
304
|
+
* @returns Map of index -> axis -> value
|
|
305
|
+
*/
|
|
306
|
+
getAnalogState(port: number): Map<number, Map<number, number>> {
|
|
307
|
+
return new Map(
|
|
308
|
+
Array.from(this.analogState.get(port) ?? []).map(([index, axes]) => [
|
|
309
|
+
index,
|
|
310
|
+
new Map(axes),
|
|
311
|
+
])
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Set a core button state directly.
|
|
317
|
+
*
|
|
318
|
+
* @param port Controller port
|
|
319
|
+
* @param coreButton Core-specific button ID
|
|
320
|
+
* @param pressed Whether pressed or released
|
|
321
|
+
*/
|
|
322
|
+
private setButton(port: number, coreButton: number, pressed: boolean): void {
|
|
323
|
+
const portState = this.portState.get(port);
|
|
324
|
+
if (!portState) {return;}
|
|
325
|
+
|
|
326
|
+
const wasPressed = portState.get(coreButton) ?? false;
|
|
327
|
+
if (pressed !== wasPressed) {
|
|
328
|
+
portState.set(coreButton, pressed);
|
|
329
|
+
this.onButtonChange?.(port, coreButton, pressed);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the current button state for a port.
|
|
335
|
+
*
|
|
336
|
+
* @param port Controller port
|
|
337
|
+
* @returns Map of core button ID to pressed state
|
|
338
|
+
*/
|
|
339
|
+
getButtonState(port: number): Map<number, boolean> {
|
|
340
|
+
return new Map(this.portState.get(port) ?? []);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get pressed buttons as a display string.
|
|
345
|
+
*
|
|
346
|
+
* @param port Controller port
|
|
347
|
+
* @returns Space-separated button names (with arrow characters for D-pad)
|
|
348
|
+
*/
|
|
349
|
+
getPressedButtons(port: number = 0): string {
|
|
350
|
+
const portState = this.portState.get(port);
|
|
351
|
+
if (!portState) {return '';}
|
|
352
|
+
|
|
353
|
+
// Map direction names to Unicode arrows
|
|
354
|
+
const formatButtonName = (name: string): string => {
|
|
355
|
+
switch (name.toLowerCase()) {
|
|
356
|
+
case 'up': return '↑';
|
|
357
|
+
case 'down': return '↓';
|
|
358
|
+
case 'left': return '←';
|
|
359
|
+
case 'right': return '→';
|
|
360
|
+
default: return name;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return pipe(
|
|
365
|
+
this.coreButtons,
|
|
366
|
+
filter((button) => portState.get(button.id) === true),
|
|
367
|
+
map((button) => formatButtonName(button.name))
|
|
368
|
+
).join(' ');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Clear all button and analog states (e.g., when losing focus).
|
|
373
|
+
*/
|
|
374
|
+
clear(): void {
|
|
375
|
+
for (const portState of this.portState.values()) {
|
|
376
|
+
for (const [button, pressed] of portState) {
|
|
377
|
+
if (pressed) {
|
|
378
|
+
portState.set(button, false);
|
|
379
|
+
// Note: We don't call onButtonChange here to avoid spamming
|
|
380
|
+
// the core during focus loss. The core should handle this gracefully.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Clear analog states
|
|
386
|
+
for (const portAnalog of this.analogState.values()) {
|
|
387
|
+
for (const stickState of portAnalog.values()) {
|
|
388
|
+
stickState.set(ANALOG_AXIS.X, 0);
|
|
389
|
+
stickState.set(ANALOG_AXIS.Y, 0);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Set a custom keyboard mapping.
|
|
396
|
+
*
|
|
397
|
+
* @param key Keyboard key
|
|
398
|
+
* @param standardButton StandardButton to map to, or undefined to remove
|
|
399
|
+
*/
|
|
400
|
+
setKeyMapping(key: string, standardButton: StandardButton | undefined): void {
|
|
401
|
+
if (standardButton === undefined) {
|
|
402
|
+
this.keyboardMap.delete(key);
|
|
403
|
+
} else {
|
|
404
|
+
this.keyboardMap.set(key, standardButton);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get the keyboard mapping.
|
|
410
|
+
*
|
|
411
|
+
* @returns Copy of the keyboard map
|
|
412
|
+
*/
|
|
413
|
+
getKeyboardMap(): Map<string, StandardButton> {
|
|
414
|
+
return new Map(this.keyboardMap);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Check if a standard button is mapped to a core button.
|
|
419
|
+
*
|
|
420
|
+
* @param standardButton The standard button to check
|
|
421
|
+
* @returns true if mapped
|
|
422
|
+
*/
|
|
423
|
+
isButtonMapped(standardButton: StandardButton): boolean {
|
|
424
|
+
return this.standardToCore.has(standardButton);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the core button ID for a standard button.
|
|
429
|
+
*
|
|
430
|
+
* @param standardButton The standard button
|
|
431
|
+
* @returns Core button ID or undefined if not mapped
|
|
432
|
+
*/
|
|
433
|
+
getCoreButton(standardButton: StandardButton): number | undefined {
|
|
434
|
+
return this.standardToCore.get(standardButton);
|
|
435
|
+
}
|
|
436
|
+
}
|