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.
Files changed (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. package/vitest.config.ts +13 -0
@@ -0,0 +1,425 @@
1
+ # Multi-Core Architecture - Technical Requirements Document
2
+
3
+ This document describes the multi-core architecture of emoemu, which supports any system via libretro cores.
4
+
5
+ ## Overview
6
+
7
+ ### Goals
8
+
9
+ 1. **Modularity**: Separate system-specific emulation (cores) from shared infrastructure (frontend)
10
+ 2. **Extensibility**: Enable adding new systems via libretro cores without modifying shared code
11
+ 3. **Code Reuse**: Share input handling, rendering, audio output, and state management across cores
12
+ 4. **Consistency**: Provide a unified user experience regardless of which system is being emulated
13
+
14
+ ### Architecture Model
15
+
16
+ The architecture follows the **libretro model** with a clear separation between:
17
+ - **Cores**: System-specific emulation (CPU, PPU, APU, memory, cartridge/ROM)
18
+ - **Frontend**: Shared infrastructure (CLI, input, rendering, audio output, state management)
19
+
20
+ Unlike libretro's C ABI approach, this TypeScript implementation uses interfaces and dependency injection for type safety.
21
+
22
+ ---
23
+
24
+ ## Core Interface
25
+
26
+ ### SystemInfo
27
+
28
+ Describes a core's capabilities and requirements. Called before loading a ROM.
29
+
30
+ ```typescript
31
+ interface SystemInfo {
32
+ /** Unique identifier (e.g., "nes", "gbc") */
33
+ id: string;
34
+
35
+ /** Human-readable name (e.g., "Nintendo Entertainment System") */
36
+ name: string;
37
+
38
+ /** File extensions this core handles (e.g., [".nes", ".unf"]) */
39
+ extensions: string[];
40
+
41
+ /** Native framebuffer dimensions */
42
+ width: number;
43
+ height: number;
44
+
45
+ /** Target frames per second (e.g., 60.0988 for NES NTSC) */
46
+ fps: number;
47
+
48
+ /** Preferred audio sample rate in Hz */
49
+ sampleRate: number;
50
+
51
+ /** Pixel aspect ratio for correct display (e.g., 8/7 for NES) */
52
+ pixelAspectRatio: number;
53
+
54
+ /** Maximum controller ports */
55
+ maxPlayers: number;
56
+
57
+ /** Button definitions for this system */
58
+ buttons: ButtonDefinition[];
59
+
60
+ /** Framebuffer color format */
61
+ colorSpace: 'rgb15' | 'rgb24';
62
+ }
63
+ ```
64
+
65
+ ### ButtonDefinition
66
+
67
+ Defines a button for input mapping.
68
+
69
+ ```typescript
70
+ interface ButtonDefinition {
71
+ /** Button ID (0-based index used in setButtonState) */
72
+ id: number;
73
+
74
+ /** Display name (e.g., "A", "Start", "L") */
75
+ name: string;
76
+
77
+ /** Suggested keyboard key */
78
+ defaultKey: string;
79
+
80
+ /** Suggested gamepad button */
81
+ defaultGamepad: string;
82
+ }
83
+ ```
84
+
85
+ ### AudioConfig
86
+
87
+ Audio output configuration.
88
+
89
+ ```typescript
90
+ interface AudioConfig {
91
+ sampleRate: number;
92
+ channels: 1 | 2; // Mono or stereo
93
+ }
94
+ ```
95
+
96
+ ### Core Interface
97
+
98
+ Main interface that all system emulators implement.
99
+
100
+ ```typescript
101
+ interface Core {
102
+ //=== Lifecycle ===
103
+
104
+ /** Get system capabilities (can be called before loadRom) */
105
+ getSystemInfo(): SystemInfo;
106
+
107
+ /** Load a ROM file */
108
+ loadRom(romPath: string): void;
109
+
110
+ /** Reset to power-on state */
111
+ reset(): void;
112
+
113
+ /** Clean up resources */
114
+ destroy(): void;
115
+
116
+ //=== Emulation ===
117
+
118
+ /** Run one frame of emulation */
119
+ runFrame(): void;
120
+
121
+ /** Check if frame completed (for variable-rate systems) */
122
+ isFrameComplete(): boolean;
123
+
124
+ //=== Video Output ===
125
+
126
+ /** Get current framebuffer (format per SystemInfo.colorSpace) */
127
+ getFramebuffer(): Uint8Array | Uint16Array;
128
+
129
+ //=== Audio Output ===
130
+
131
+ /** Get audio configuration */
132
+ getAudioConfig(): AudioConfig;
133
+
134
+ /** Set callback for audio samples */
135
+ setAudioCallback(callback: ((samples: Float32Array) => void) | null): void;
136
+
137
+ //=== Input ===
138
+
139
+ /** Set button state for a controller port */
140
+ setButtonState(port: number, button: number, pressed: boolean): void;
141
+
142
+ /** Get current button state (for status display) */
143
+ getButtonState(port: number): Map<number, boolean>;
144
+
145
+ //=== State Management ===
146
+
147
+ /** Serialize current state (raw binary) */
148
+ getState(): Buffer | null;
149
+
150
+ /** Restore from saved state (raw binary) */
151
+ setState(state: Buffer): void;
152
+
153
+ //=== Battery/SRAM (Optional) ===
154
+
155
+ /** Check for battery-backed saves */
156
+ hasBatterySave(): boolean;
157
+
158
+ /** Get battery RAM for saving */
159
+ getBatteryRam(): Uint8Array | null;
160
+
161
+ /** Load battery RAM from disk */
162
+ setBatteryRam(data: Uint8Array): void;
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Design Decisions
169
+
170
+ ### Timing Abstraction
171
+
172
+ **Decision**: `runFrame()` abstracts internal timing. Each core manages its own CPU/PPU synchronization. The frontend uses `SystemInfo.fps` for frame pacing. Libretro cores handle their own timing internally.
173
+
174
+ ### Input Mapping
175
+
176
+ Different systems have different button counts:
177
+ - **NES**: 8 buttons (A, B, Select, Start, D-pad)
178
+ - **SNES-style (libretro)**: 12 buttons (A, B, X, Y, L, R, Select, Start, D-pad)
179
+
180
+ **Decision**: Frontend defines `StandardButton` enum for physical inputs. `InputMapper` translates to core-specific button IDs via name matching.
181
+
182
+ ```typescript
183
+ enum StandardButton {
184
+ A, B, X, Y, // Face buttons
185
+ L, R, L2, R2, // Shoulder buttons
186
+ Start, Select, // Control buttons
187
+ Up, Down, Left, Right // D-pad
188
+ }
189
+ ```
190
+
191
+ ### Color Space Handling
192
+
193
+ **Decision**: `SystemInfo.colorSpace` specifies format (`'rgb15'` or `'rgb24'`). Renderers convert to RGB24 for display. Libretro cores output various formats (XRGB8888, RGB565, XRGB1555) which are normalized to rgb15 or rgb24.
194
+
195
+ ### Audio Output
196
+
197
+ **Decision**: Callback-based audio with configurable sample rate. Frontend's RtAudio wrapper handles the output device. Libretro cores output stereo audio at various sample rates.
198
+
199
+ ### State Management
200
+
201
+ **Decision**: All cores use raw binary save states (compatible with RetroArch). The frontend handles save/load as opaque `Buffer` data.
202
+
203
+ ---
204
+
205
+ ## Directory Structure
206
+
207
+ ### Proposed Layout
208
+
209
+ ```
210
+ src/
211
+ ├── index.ts # CLI entry point, main loop
212
+ ├── cli/ # CLI argument parsing and commands
213
+ │ ├── parseArgs/ # Argument parsing, config-to-options mapping
214
+ │ ├── commands/ # CLI commands (usage, install-core, playlist, etc.)
215
+ │ └── runEmulator/ # Emulator launch, state file validation
216
+
217
+ ├── Emulator/ # Main emulation loop, renderer orchestration
218
+ │ ├── saveState/ # Save state and battery save (.srm) management
219
+ │ ├── screenshot/ # Screenshot capture, thumbnails
220
+ │ └── terminalDimensions/ # Terminal display size calculation
221
+
222
+ ├── core/ # Core interface definitions (Core, SystemInfo, AudioConfig)
223
+
224
+ ├── frontend/ # Shared infrastructure (audio, notifications, state)
225
+
226
+ ├── input/ # Keyboard (Kitty protocol) and gamepad (node-hid) handling
227
+
228
+ ├── rendering/ # Kitty graphics, Unicode half-blocks, ASCII, emoji renderers
229
+
230
+ ├── cores/
231
+ │ └── libretro/ # Libretro core wrapper
232
+ │ ├── index.ts # LibretroCore class (implements Core interface)
233
+ │ ├── api/ # FFI bindings using koffi
234
+ │ ├── environment/ # Environment callback handler
235
+ │ └── ... # Callbacks, pixel format, loader, etc.
236
+
237
+ ├── netplay/ # RetroArch-compatible netplay (rollback, LAN discovery)
238
+
239
+ ├── ui/ # React/Ink TUI
240
+ │ └── RomBrowser/ # ROM browser, settings, netplay panels
241
+
242
+ └── types/
243
+ └── *.d.ts # Type declarations
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Core Registry
249
+
250
+ Discovers and instantiates cores based on ROM file extension.
251
+
252
+ ```typescript
253
+ // src/frontend/core-registry.ts
254
+
255
+ interface CoreFactory {
256
+ create(): Core;
257
+ extensions: string[];
258
+ }
259
+
260
+ const cores = new Map<string, CoreFactory>([
261
+ // Libretro cores are registered dynamically via loader.ts
262
+ ]);
263
+
264
+ /** Auto-detect core from file extension */
265
+ function detectCore(romPath: string): Core | null {
266
+ const ext = romPath.toLowerCase().match(/\.[^.]+$/)?.[0];
267
+ for (const [, factory] of cores) {
268
+ if (factory.extensions.includes(ext)) {
269
+ return factory.create();
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+
275
+ /** Get core by ID */
276
+ function getCore(id: string): Core | null {
277
+ return cores.get(id)?.create() ?? null;
278
+ }
279
+
280
+ /** List available cores */
281
+ function listCores(): Array<{ id: string; name: string; extensions: string[] }>;
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Frontend Emulator
287
+
288
+ The shared frontend orchestrates the core and handles I/O.
289
+
290
+ ```typescript
291
+ // src/frontend/emulator.ts
292
+
293
+ class Emulator {
294
+ private core: Core;
295
+ private systemInfo: SystemInfo;
296
+ private renderer: Renderer;
297
+ private audioManager: AudioManager;
298
+ private stateManager: StateManager;
299
+ private inputMapper: InputMapper;
300
+
301
+ constructor(options: FrontendOptions) {
302
+ // Auto-detect or use provided core
303
+ this.core = options.core ?? detectCore(options.romPath);
304
+ this.systemInfo = this.core.getSystemInfo();
305
+
306
+ // Initialize shared infrastructure
307
+ this.renderer = createRenderer(options.renderMode, this.systemInfo);
308
+ this.audioManager = new AudioManager(this.core.getAudioConfig());
309
+ this.stateManager = new StateManager(options.romPath, this.systemInfo.id);
310
+ this.inputMapper = new InputMapper(this.systemInfo.buttons);
311
+
312
+ // Load ROM and wire callbacks
313
+ this.core.loadRom(options.romPath);
314
+ this.core.setAudioCallback((s) => this.audioManager.pushSamples(s));
315
+ this.inputMapper.onButtonChange = (port, btn, pressed) =>
316
+ this.core.setButtonState(port, btn, pressed);
317
+ }
318
+
319
+ async run(): Promise<void> {
320
+ const targetFrameTime = 1000 / this.systemInfo.fps;
321
+
322
+ // Main loop
323
+ while (this.running) {
324
+ const start = performance.now();
325
+
326
+ this.inputMapper.update();
327
+ this.core.runFrame();
328
+ this.renderFrame();
329
+ this.updateStatusBar();
330
+
331
+ // Frame pacing
332
+ const elapsed = performance.now() - start;
333
+ if (elapsed < targetFrameTime) {
334
+ await sleep(targetFrameTime - elapsed);
335
+ }
336
+ }
337
+ }
338
+
339
+ private renderFrame(): void {
340
+ const fb = this.core.getFramebuffer();
341
+ const output = this.renderer.render(fb, this.systemInfo);
342
+ process.stdout.write(output);
343
+ }
344
+ }
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Input Mapper
350
+
351
+ Translates physical inputs to core-specific buttons.
352
+
353
+ ```typescript
354
+ // src/input/input-mapper.ts
355
+
356
+ class InputMapper {
357
+ private coreButtons: ButtonDefinition[];
358
+ private standardToCore: Map<StandardButton, number>;
359
+ private portState: Map<number, Map<number, boolean>>;
360
+
361
+ onButtonChange?: (port: number, button: number, pressed: boolean) => void;
362
+
363
+ constructor(coreButtons: ButtonDefinition[]) {
364
+ this.coreButtons = coreButtons;
365
+ this.standardToCore = this.buildMapping();
366
+ }
367
+
368
+ /** Build default mapping from StandardButton to core buttons by name */
369
+ private buildMapping(): Map<StandardButton, number> {
370
+ const map = new Map<StandardButton, number>();
371
+
372
+ for (const btn of this.coreButtons) {
373
+ const name = btn.name.toLowerCase();
374
+ if (name === 'a') map.set(StandardButton.A, btn.id);
375
+ else if (name === 'b') map.set(StandardButton.B, btn.id);
376
+ else if (name === 'l') map.set(StandardButton.L, btn.id);
377
+ else if (name === 'r') map.set(StandardButton.R, btn.id);
378
+ else if (name === 'start') map.set(StandardButton.Start, btn.id);
379
+ else if (name === 'select') map.set(StandardButton.Select, btn.id);
380
+ else if (name === 'up') map.set(StandardButton.Up, btn.id);
381
+ else if (name === 'down') map.set(StandardButton.Down, btn.id);
382
+ else if (name === 'left') map.set(StandardButton.Left, btn.id);
383
+ else if (name === 'right') map.set(StandardButton.Right, btn.id);
384
+ }
385
+
386
+ return map;
387
+ }
388
+
389
+ /** Handle keyboard input */
390
+ handleKey(key: string, pressed: boolean, port = 0): void {
391
+ const standard = this.keyToStandard(key);
392
+ if (standard === undefined) return;
393
+
394
+ const coreButton = this.standardToCore.get(standard);
395
+ if (coreButton === undefined) return;
396
+
397
+ this.setButton(port, coreButton, pressed);
398
+ }
399
+
400
+ private setButton(port: number, button: number, pressed: boolean): void {
401
+ const state = this.portState.get(port);
402
+ if (state?.get(button) !== pressed) {
403
+ state?.set(button, pressed);
404
+ this.onButtonChange?.(port, button, pressed);
405
+ }
406
+ }
407
+ }
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Appendix: Adding New Systems
413
+
414
+ New systems can be added via libretro cores without modifying emoemu code:
415
+
416
+ 1. Download a libretro core from [RetroArch buildbot](https://buildbot.libretro.com/nightly/)
417
+ 2. Place in `~/Library/Application Support/emoemu/cores/` (macOS), `~/.config/emoemu/cores/` (Linux), or `%APPDATA%\emoemu\cores\` (Windows)
418
+ 3. The core will be auto-detected on next run
419
+
420
+ Popular libretro cores:
421
+ - **picodrive**: Sega Genesis/Mega Drive, Master System, Game Gear
422
+ - **mgba**: Game Boy Advance
423
+ - **gambatte**: Game Boy / Game Boy Color
424
+ - **bsnes** / **snes9x**: Super Nintendo
425
+ - **mednafen_pce**: PC Engine / TurboGrafx-16