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,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
|