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,665 @@
|
|
|
1
|
+
# RetroArch Netplay Support - Technical Requirements Document
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document outlines the implementation plan for adding RetroArch-compatible netplay support to emoemu. The implementation will allow emoemu to act as both a netplay server (host) and client, enabling multiplayer gaming over the network with other emoemu instances and potentially RetroArch clients.
|
|
6
|
+
|
|
7
|
+
**Scope**: Libretro cores only. Native NES core is explicitly excluded from netplay support.
|
|
8
|
+
|
|
9
|
+
## Implementation Status
|
|
10
|
+
|
|
11
|
+
| Milestone | Status | Description |
|
|
12
|
+
|-----------|--------|-------------|
|
|
13
|
+
| 1. Protocol Foundation | ✅ Complete | Protocol constants, command encoding/decoding, TCP connection wrapper |
|
|
14
|
+
| 2. Frame Buffer & State Management | ✅ Complete | CRC32, frame buffer ring, input buffer with prediction |
|
|
15
|
+
| 3. Sync Manager & Rollback | ✅ Complete | Rollback coordination, desync detection, input merging |
|
|
16
|
+
| 4. Server Implementation | ✅ Complete | NetplayServer class with handshake, input relay, client management |
|
|
17
|
+
| 5. Client Implementation | ✅ Complete | NetplayClient class with handshake, input exchange, rollback integration |
|
|
18
|
+
| 6. Emulator Integration | ✅ Complete | CLI arguments, status bar, notifications, ROM CRC validation |
|
|
19
|
+
| 7. Testing & Polish | ✅ Complete | 212 tests passing, documentation updated |
|
|
20
|
+
| 8. Documentation & Release | ✅ Complete | CLAUDE.md and README.md updated |
|
|
21
|
+
|
|
22
|
+
**Total Tests**: 212 passing (49 protocol + 57 frame buffer + 30 sync manager + 76 other)
|
|
23
|
+
|
|
24
|
+
## Background
|
|
25
|
+
|
|
26
|
+
### RetroArch Netplay Protocol
|
|
27
|
+
|
|
28
|
+
RetroArch's netplay uses a deterministic lockstep model with rollback:
|
|
29
|
+
|
|
30
|
+
- **Transport**: TCP on port 55435 (reliable, in-order delivery required)
|
|
31
|
+
- **Architecture**: Server is canonical for synchronization; supports up to 32 clients
|
|
32
|
+
- **Synchronization**: Input delay + rollback/replay when delayed input arrives
|
|
33
|
+
- **State Format**: Raw binary savestates from `retro_serialize()`
|
|
34
|
+
|
|
35
|
+
### Key Protocol Concepts
|
|
36
|
+
|
|
37
|
+
1. **Frame Buffer Ring**: Maintains history of frames with input and serialized state
|
|
38
|
+
2. **Three Frame Pointers**:
|
|
39
|
+
- `self`: Current local execution frame
|
|
40
|
+
- `other`: Last perfectly synchronized frame
|
|
41
|
+
- `unread`: First frame with incomplete remote input
|
|
42
|
+
3. **Rollback**: When late input arrives, rewind to `other`, replay with correct input
|
|
43
|
+
|
|
44
|
+
### Command Protocol
|
|
45
|
+
|
|
46
|
+
Each command consists of:
|
|
47
|
+
- 32-bit command ID (network byte order)
|
|
48
|
+
- 32-bit payload size (network byte order)
|
|
49
|
+
- Variable payload
|
|
50
|
+
|
|
51
|
+
Key commands:
|
|
52
|
+
| Command | ID | Description |
|
|
53
|
+
|---------|-----|-------------|
|
|
54
|
+
| `INPUT` | 0x0003 | Per-frame input data (required every frame) |
|
|
55
|
+
| `NOINPUT` | 0x0004 | Server frame advance without input |
|
|
56
|
+
| `NICK` | 0x0020 | Nickname exchange |
|
|
57
|
+
| `PASSWORD` | 0x0021 | Authentication (SHA-256 hash) |
|
|
58
|
+
| `INFO` | 0x0022 | Core name, version, content CRC |
|
|
59
|
+
| `SYNC` | 0x0023 | Initial state synchronization |
|
|
60
|
+
| `MODE` | 0x0026 | Player mode changes (play/spectate) |
|
|
61
|
+
| `CRC` | 0x0040 | Frame hash for desync detection |
|
|
62
|
+
| `LOAD_SAVESTATE` | 0x0042 | State synchronization |
|
|
63
|
+
| `PAUSE` | 0x0043 | Pause notification |
|
|
64
|
+
| `RESUME` | 0x0044 | Resume notification |
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
### Functional Requirements
|
|
69
|
+
|
|
70
|
+
#### Server (Host) Mode
|
|
71
|
+
- FR-1: Accept incoming TCP connections on configurable port (default: 55435)
|
|
72
|
+
- FR-2: Perform handshake with clients (header, nick, password, info, sync)
|
|
73
|
+
- FR-3: Validate client compatibility (core name, core version, content CRC)
|
|
74
|
+
- FR-4: Send initial savestate to synchronize new clients
|
|
75
|
+
- FR-5: Relay input from all clients to all other clients
|
|
76
|
+
- FR-6: Periodically send CRC commands for desync detection
|
|
77
|
+
- FR-7: Handle client disconnection gracefully
|
|
78
|
+
- FR-8: Support optional password protection
|
|
79
|
+
- FR-9: Support spectator mode (receive state/input, no input sent)
|
|
80
|
+
|
|
81
|
+
#### Client Mode
|
|
82
|
+
- FR-10: Connect to server via hostname/IP and port
|
|
83
|
+
- FR-11: Perform handshake with server
|
|
84
|
+
- FR-12: Load initial savestate from server
|
|
85
|
+
- FR-13: Send local input every frame
|
|
86
|
+
- FR-14: Receive and apply remote input
|
|
87
|
+
- FR-15: Perform rollback/replay when input arrives late
|
|
88
|
+
- FR-16: Request savestate resync on desync detection
|
|
89
|
+
- FR-17: Support spectator mode
|
|
90
|
+
|
|
91
|
+
#### Input Handling
|
|
92
|
+
- FR-18: Buffer local input with configurable delay (0-16 frames)
|
|
93
|
+
- FR-19: Simulate remote input when not yet received (repeat last input)
|
|
94
|
+
- FR-20: Support all libretro input devices (joypad, analog)
|
|
95
|
+
|
|
96
|
+
#### State Management
|
|
97
|
+
- FR-21: Maintain ring buffer of recent frame states (configurable depth)
|
|
98
|
+
- FR-22: Serialize/deserialize state via libretro API
|
|
99
|
+
- FR-23: Compute CRC32 of serialized state for comparison
|
|
100
|
+
- FR-24: Support zlib compression for large state transfers
|
|
101
|
+
|
|
102
|
+
### Non-Functional Requirements
|
|
103
|
+
|
|
104
|
+
- NFR-1: Latency: Support playable experience up to 200ms RTT
|
|
105
|
+
- NFR-2: Memory: State buffer should not exceed 256MB for typical cores
|
|
106
|
+
- NFR-3: CPU: Rollback should complete within frame budget (16ms at 60fps)
|
|
107
|
+
- NFR-4: Compatibility: Wire-compatible with RetroArch netplay protocol
|
|
108
|
+
|
|
109
|
+
### Out of Scope
|
|
110
|
+
|
|
111
|
+
- Native NES core netplay support
|
|
112
|
+
- Lobby server integration (manual IP/hostname entry only)
|
|
113
|
+
- NAT traversal / hole punching
|
|
114
|
+
- Link-cable emulation (GB/GBA/PSP)
|
|
115
|
+
- Hardware-rendered cores (OpenGL/Vulkan)
|
|
116
|
+
|
|
117
|
+
## Architecture
|
|
118
|
+
|
|
119
|
+
### New Directory Structure
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/
|
|
123
|
+
├── netplay/
|
|
124
|
+
│ ├── index.ts # Module exports
|
|
125
|
+
│ ├── consts.ts # Protocol constants and command IDs
|
|
126
|
+
│ ├── types.ts # TypeScript interfaces
|
|
127
|
+
│ ├── protocol.ts # Command serialization/deserialization
|
|
128
|
+
│ ├── connection.ts # TCP connection wrapper
|
|
129
|
+
│ ├── server.ts # NetplayServer class
|
|
130
|
+
│ ├── client.ts # NetplayClient class
|
|
131
|
+
│ ├── frame-buffer.ts # Ring buffer for frame history
|
|
132
|
+
│ ├── input-buffer.ts # Input state management
|
|
133
|
+
│ └── sync-manager.ts # Rollback and replay logic
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Core Components
|
|
137
|
+
|
|
138
|
+
#### 1. Protocol Layer (`protocol.ts`)
|
|
139
|
+
|
|
140
|
+
Handles command encoding/decoding:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
interface NetplayCommand {
|
|
144
|
+
cmd: NetplayCommandId;
|
|
145
|
+
payload: Buffer;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Serialize command to wire format
|
|
149
|
+
const encodeCommand = (cmd: NetplayCommand): Buffer => { ... };
|
|
150
|
+
|
|
151
|
+
// Parse command from buffer (handles partial reads)
|
|
152
|
+
const decodeCommand = (buffer: Buffer): { command: NetplayCommand; bytesConsumed: number } | null => { ... };
|
|
153
|
+
|
|
154
|
+
// Specific command builders
|
|
155
|
+
const buildInputCommand = (frame: number, clientId: number, input: Uint32Array): Buffer => { ... };
|
|
156
|
+
const buildInfoCommand = (coreName: string, coreVersion: string, contentCrc: number): Buffer => { ... };
|
|
157
|
+
const buildSyncCommand = (frame: number, state: Buffer, players: number): Buffer => { ... };
|
|
158
|
+
// ... etc
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### 2. Connection Manager (`connection.ts`)
|
|
162
|
+
|
|
163
|
+
Wraps TCP socket with buffered reads:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
interface NetplayConnection {
|
|
167
|
+
readonly id: number;
|
|
168
|
+
readonly address: string;
|
|
169
|
+
readonly port: number;
|
|
170
|
+
|
|
171
|
+
send(command: NetplayCommand): Promise<void>;
|
|
172
|
+
receive(): AsyncGenerator<NetplayCommand>;
|
|
173
|
+
close(): void;
|
|
174
|
+
|
|
175
|
+
// Connection state
|
|
176
|
+
nickname: string;
|
|
177
|
+
clientNumber: number;
|
|
178
|
+
mode: 'playing' | 'spectating';
|
|
179
|
+
latency: number;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### 3. Frame Buffer (`frame-buffer.ts`)
|
|
184
|
+
|
|
185
|
+
Ring buffer storing frame history for rollback:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface FrameState {
|
|
189
|
+
frameNumber: number;
|
|
190
|
+
serializedState: Buffer | null; // May be null if not yet captured
|
|
191
|
+
localInput: Uint32Array;
|
|
192
|
+
remoteInput: Map<number, Uint32Array>; // clientId -> input
|
|
193
|
+
crc: number | null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface FrameBuffer {
|
|
197
|
+
readonly capacity: number;
|
|
198
|
+
|
|
199
|
+
// Access frames
|
|
200
|
+
get(frameNumber: number): FrameState | null;
|
|
201
|
+
getCurrent(): FrameState;
|
|
202
|
+
|
|
203
|
+
// Frame management
|
|
204
|
+
advance(): FrameState; // Move to next frame
|
|
205
|
+
setLocalInput(input: Uint32Array): void;
|
|
206
|
+
setRemoteInput(clientId: number, frameNumber: number, input: Uint32Array): void;
|
|
207
|
+
setState(frameNumber: number, state: Buffer): void;
|
|
208
|
+
|
|
209
|
+
// Rollback support
|
|
210
|
+
findRollbackFrame(): number; // Earliest frame needing replay
|
|
211
|
+
getStateForFrame(frameNumber: number): Buffer | null;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### 4. Sync Manager (`sync-manager.ts`)
|
|
216
|
+
|
|
217
|
+
Coordinates rollback and replay:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
interface SyncManager {
|
|
221
|
+
// Frame tracking
|
|
222
|
+
readonly selfFrame: number;
|
|
223
|
+
readonly otherFrame: number; // Last synced frame
|
|
224
|
+
readonly unreadFrame: number; // First frame with missing input
|
|
225
|
+
|
|
226
|
+
// State management
|
|
227
|
+
captureState(): void; // Serialize current core state
|
|
228
|
+
restoreState(frameNumber: number): void; // Load state for rollback
|
|
229
|
+
|
|
230
|
+
// Sync operations
|
|
231
|
+
needsRollback(): boolean;
|
|
232
|
+
performRollback(): void; // Rewind and replay
|
|
233
|
+
|
|
234
|
+
// Input
|
|
235
|
+
getInputForFrame(frameNumber: number, port: number): Uint32Array;
|
|
236
|
+
simulateInput(lastKnown: Uint32Array): Uint32Array; // Predict input
|
|
237
|
+
|
|
238
|
+
// Desync detection
|
|
239
|
+
computeFrameCrc(frameNumber: number): number;
|
|
240
|
+
checkDesync(remoteCrc: number, frameNumber: number): boolean;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### 5. Server (`server.ts`)
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
interface NetplayServerOptions {
|
|
248
|
+
port: number;
|
|
249
|
+
password?: string;
|
|
250
|
+
maxClients: number;
|
|
251
|
+
inputLatencyFrames: number;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface NetplayServer {
|
|
255
|
+
// Lifecycle
|
|
256
|
+
start(core: LibretroCore, romPath: string): Promise<void>;
|
|
257
|
+
stop(): void;
|
|
258
|
+
|
|
259
|
+
// Client management
|
|
260
|
+
readonly clients: ReadonlyMap<number, NetplayConnection>;
|
|
261
|
+
kick(clientId: number, reason: string): void;
|
|
262
|
+
|
|
263
|
+
// Frame execution (called by emulator loop)
|
|
264
|
+
preFrame(): void; // Gather input, check for new connections
|
|
265
|
+
postFrame(): void; // Broadcast input, check sync
|
|
266
|
+
|
|
267
|
+
// Events
|
|
268
|
+
on(event: 'client-connected', handler: (client: NetplayConnection) => void): void;
|
|
269
|
+
on(event: 'client-disconnected', handler: (client: NetplayConnection) => void): void;
|
|
270
|
+
on(event: 'desync', handler: (clientId: number, frame: number) => void): void;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### 6. Client (`client.ts`)
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
interface NetplayClientOptions {
|
|
278
|
+
host: string;
|
|
279
|
+
port: number;
|
|
280
|
+
password?: string;
|
|
281
|
+
nickname: string;
|
|
282
|
+
inputLatencyFrames: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface NetplayClient {
|
|
286
|
+
// Lifecycle
|
|
287
|
+
connect(core: LibretroCore): Promise<void>;
|
|
288
|
+
disconnect(): void;
|
|
289
|
+
|
|
290
|
+
// State
|
|
291
|
+
readonly connected: boolean;
|
|
292
|
+
readonly serverInfo: { coreName: string; coreVersion: string; contentCrc: number } | null;
|
|
293
|
+
|
|
294
|
+
// Frame execution
|
|
295
|
+
preFrame(): void; // Send input, receive remote input
|
|
296
|
+
postFrame(): void; // Handle sync, check for rollback
|
|
297
|
+
|
|
298
|
+
// Events
|
|
299
|
+
on(event: 'connected', handler: () => void): void;
|
|
300
|
+
on(event: 'disconnected', handler: (reason: string) => void): void;
|
|
301
|
+
on(event: 'desync', handler: (frame: number) => void): void;
|
|
302
|
+
on(event: 'rollback', handler: (frames: number) => void): void;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Integration with Emulator
|
|
307
|
+
|
|
308
|
+
The `Emulator` class will be extended to support netplay:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// New methods in Emulator class
|
|
312
|
+
interface EmulatorNetplayMethods {
|
|
313
|
+
startNetplayServer(options: NetplayServerOptions): Promise<void>;
|
|
314
|
+
connectToNetplay(options: NetplayClientOptions): Promise<void>;
|
|
315
|
+
disconnectNetplay(): void;
|
|
316
|
+
isNetplayActive(): boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Modified run loop (pseudo-code)
|
|
320
|
+
const runFrameWithNetplay = (): void => {
|
|
321
|
+
if (this.netplay) {
|
|
322
|
+
this.netplay.preFrame(); // Gather/exchange input
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (this.syncManager?.needsRollback()) {
|
|
326
|
+
this.disableAudio(); // Prevent audio artifacts
|
|
327
|
+
this.syncManager.performRollback();
|
|
328
|
+
this.enableAudio();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.syncInputToCore(); // Apply merged local+remote input
|
|
332
|
+
this.core.runFrame();
|
|
333
|
+
|
|
334
|
+
if (this.netplay) {
|
|
335
|
+
this.netplay.postFrame(); // Broadcast state, check sync
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### CLI Interface
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Host a netplay session
|
|
344
|
+
emoemu game.sfc --netplay-host [--netplay-port 55435] [--netplay-password secret]
|
|
345
|
+
|
|
346
|
+
# Connect to a netplay session
|
|
347
|
+
emoemu game.sfc --netplay-connect hostname[:port] [--netplay-password secret]
|
|
348
|
+
|
|
349
|
+
# Additional options
|
|
350
|
+
--netplay-spectate # Join as spectator (no input)
|
|
351
|
+
--netplay-nick "Player1" # Set nickname
|
|
352
|
+
--netplay-frames 2 # Input delay frames (0-16, default: 2)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Implementation Plan
|
|
356
|
+
|
|
357
|
+
### Milestone 1: Protocol Foundation (Week 1-2)
|
|
358
|
+
|
|
359
|
+
**Goal**: Implement core protocol primitives and basic TCP communication.
|
|
360
|
+
|
|
361
|
+
**Tasks**:
|
|
362
|
+
1. Create `src/netplay/` directory structure
|
|
363
|
+
2. Define protocol constants (`consts.ts`)
|
|
364
|
+
- Command IDs (INPUT, NOINPUT, NICK, PASSWORD, INFO, SYNC, etc.)
|
|
365
|
+
- Default port, timing constants, limits
|
|
366
|
+
3. Implement command serialization/deserialization (`protocol.ts`)
|
|
367
|
+
- `encodeCommand()` / `decodeCommand()`
|
|
368
|
+
- Individual command builders (buildInputCommand, buildInfoCommand, etc.)
|
|
369
|
+
- Handle network byte order (big-endian)
|
|
370
|
+
4. Implement TCP connection wrapper (`connection.ts`)
|
|
371
|
+
- Buffered reads for partial commands
|
|
372
|
+
- Async command iteration
|
|
373
|
+
- Connection state tracking
|
|
374
|
+
5. Add TypeScript interfaces (`types.ts`)
|
|
375
|
+
|
|
376
|
+
**Deliverable**: Protocol layer that can encode/decode all netplay commands.
|
|
377
|
+
|
|
378
|
+
**Acceptance Criteria**:
|
|
379
|
+
- Unit tests for all command types
|
|
380
|
+
- Round-trip encode/decode produces identical data
|
|
381
|
+
- Handles partial TCP reads correctly
|
|
382
|
+
|
|
383
|
+
### Milestone 2: Frame Buffer & State Management (Week 2-3)
|
|
384
|
+
|
|
385
|
+
**Goal**: Implement frame history tracking and state serialization.
|
|
386
|
+
|
|
387
|
+
**Tasks**:
|
|
388
|
+
1. Implement frame buffer ring (`frame-buffer.ts`)
|
|
389
|
+
- Fixed-capacity ring buffer (default: 120 frames = 2 seconds at 60fps)
|
|
390
|
+
- Store serialized state, local input, remote input per frame
|
|
391
|
+
- Frame number wraparound handling
|
|
392
|
+
2. Implement input buffer (`input-buffer.ts`)
|
|
393
|
+
- Track input per client per frame
|
|
394
|
+
- Input prediction (repeat last known input)
|
|
395
|
+
- Input delay queue
|
|
396
|
+
3. Add CRC32 computation for state comparison
|
|
397
|
+
- Use existing CRC implementation or add lightweight one
|
|
398
|
+
4. Implement state compression (optional, for large states)
|
|
399
|
+
- zlib compression for LOAD_SAVESTATE transfers
|
|
400
|
+
- Compression threshold (e.g., only compress if > 64KB)
|
|
401
|
+
|
|
402
|
+
**Deliverable**: Frame buffer that can track 2+ seconds of frame history.
|
|
403
|
+
|
|
404
|
+
**Acceptance Criteria**:
|
|
405
|
+
- Can store and retrieve frame states by number
|
|
406
|
+
- Handles buffer wraparound correctly
|
|
407
|
+
- CRC32 matches for identical states
|
|
408
|
+
|
|
409
|
+
### Milestone 3: Sync Manager & Rollback (Week 3-4)
|
|
410
|
+
|
|
411
|
+
**Goal**: Implement deterministic rollback and replay.
|
|
412
|
+
|
|
413
|
+
**Tasks**:
|
|
414
|
+
1. Implement sync manager (`sync-manager.ts`)
|
|
415
|
+
- Track self/other/unread frame pointers
|
|
416
|
+
- Detect when rollback is needed
|
|
417
|
+
- Coordinate state capture timing
|
|
418
|
+
2. Implement rollback logic
|
|
419
|
+
- Restore state from frame buffer
|
|
420
|
+
- Replay frames with corrected input
|
|
421
|
+
- Re-capture states during replay
|
|
422
|
+
3. Implement input merging
|
|
423
|
+
- Combine local + remote input for core
|
|
424
|
+
- Handle missing remote input (simulation)
|
|
425
|
+
4. Add desync detection
|
|
426
|
+
- Periodic CRC comparison
|
|
427
|
+
- Trigger state resync on mismatch
|
|
428
|
+
5. Handle audio during rollback
|
|
429
|
+
- Mute audio output during replay
|
|
430
|
+
- Resume audio after rollback complete
|
|
431
|
+
|
|
432
|
+
**Deliverable**: Sync manager that can rollback and replay frames.
|
|
433
|
+
|
|
434
|
+
**Acceptance Criteria**:
|
|
435
|
+
- Single-player rollback test: artificially delay input, verify correct replay
|
|
436
|
+
- State restoration produces identical CRC
|
|
437
|
+
- Audio doesn't glitch during rollback
|
|
438
|
+
|
|
439
|
+
### Milestone 4: Server Implementation (Week 4-5)
|
|
440
|
+
|
|
441
|
+
**Goal**: Implement netplay server (host) functionality.
|
|
442
|
+
|
|
443
|
+
**Tasks**:
|
|
444
|
+
1. Implement server class (`server.ts`)
|
|
445
|
+
- TCP server on configurable port
|
|
446
|
+
- Accept multiple client connections
|
|
447
|
+
- Track client state (nickname, player number, mode)
|
|
448
|
+
2. Implement handshake flow (server side)
|
|
449
|
+
- Receive/verify connection header
|
|
450
|
+
- Exchange nicknames
|
|
451
|
+
- Validate password (if configured)
|
|
452
|
+
- Send INFO (core name, version, content CRC)
|
|
453
|
+
- Receive client INFO, validate compatibility
|
|
454
|
+
- Send SYNC with initial state
|
|
455
|
+
3. Implement input relay
|
|
456
|
+
- Receive INPUT from clients
|
|
457
|
+
- Broadcast INPUT to all other clients
|
|
458
|
+
- Send server's own INPUT
|
|
459
|
+
4. Implement periodic sync checks
|
|
460
|
+
- Send CRC command every N frames
|
|
461
|
+
- Handle desync (send LOAD_SAVESTATE)
|
|
462
|
+
5. Handle client lifecycle
|
|
463
|
+
- New connections during gameplay
|
|
464
|
+
- Graceful disconnection
|
|
465
|
+
- Kick functionality
|
|
466
|
+
|
|
467
|
+
**Deliverable**: Functional netplay server.
|
|
468
|
+
|
|
469
|
+
**Acceptance Criteria**:
|
|
470
|
+
- Can accept connection from RetroArch client (handshake completes)
|
|
471
|
+
- Input reaches clients within expected latency
|
|
472
|
+
- New client receives valid initial state
|
|
473
|
+
|
|
474
|
+
### Milestone 5: Client Implementation (Week 5-6)
|
|
475
|
+
|
|
476
|
+
**Goal**: Implement netplay client functionality.
|
|
477
|
+
|
|
478
|
+
**Tasks**:
|
|
479
|
+
1. Implement client class (`client.ts`)
|
|
480
|
+
- TCP connection to server
|
|
481
|
+
- Connection state machine
|
|
482
|
+
- Reconnection logic (optional)
|
|
483
|
+
2. Implement handshake flow (client side)
|
|
484
|
+
- Send connection header
|
|
485
|
+
- Exchange nicknames
|
|
486
|
+
- Send password (if required)
|
|
487
|
+
- Send INFO, receive server INFO
|
|
488
|
+
- Receive SYNC, load initial state
|
|
489
|
+
3. Implement input exchange
|
|
490
|
+
- Send local INPUT every frame
|
|
491
|
+
- Receive and buffer remote INPUT
|
|
492
|
+
- Handle INPUT from other clients (via server relay)
|
|
493
|
+
4. Integrate with sync manager
|
|
494
|
+
- Feed received input to frame buffer
|
|
495
|
+
- Trigger rollback when needed
|
|
496
|
+
5. Handle connection issues
|
|
497
|
+
- Detect timeout / disconnect
|
|
498
|
+
- Attempt graceful recovery
|
|
499
|
+
|
|
500
|
+
**Deliverable**: Functional netplay client.
|
|
501
|
+
|
|
502
|
+
**Acceptance Criteria**:
|
|
503
|
+
- Can connect to RetroArch server (handshake completes)
|
|
504
|
+
- Gameplay syncs correctly (same visual state)
|
|
505
|
+
- Handles moderate packet delay gracefully
|
|
506
|
+
|
|
507
|
+
### Milestone 6: Emulator Integration (Week 6-7)
|
|
508
|
+
|
|
509
|
+
**Goal**: Integrate netplay into main emulator loop.
|
|
510
|
+
|
|
511
|
+
**Tasks**:
|
|
512
|
+
1. Modify `Emulator` class
|
|
513
|
+
- Add netplay server/client instance
|
|
514
|
+
- Inject preFrame/postFrame hooks
|
|
515
|
+
- Modify input handling for netplay
|
|
516
|
+
2. Add CLI arguments
|
|
517
|
+
- `--netplay-host`, `--netplay-connect`
|
|
518
|
+
- `--netplay-port`, `--netplay-password`
|
|
519
|
+
- `--netplay-spectate`, `--netplay-nick`
|
|
520
|
+
- `--netplay-frames`
|
|
521
|
+
3. Add status bar integration
|
|
522
|
+
- Show connection status
|
|
523
|
+
- Show ping/latency
|
|
524
|
+
- Show player count
|
|
525
|
+
4. Add notifications
|
|
526
|
+
- Player connected/disconnected
|
|
527
|
+
- Desync detected/recovered
|
|
528
|
+
- Connection lost
|
|
529
|
+
5. Handle ROM validation
|
|
530
|
+
- Compute content CRC on load
|
|
531
|
+
- Verify CRC matches server
|
|
532
|
+
|
|
533
|
+
**Deliverable**: Playable netplay from CLI.
|
|
534
|
+
|
|
535
|
+
**Acceptance Criteria**:
|
|
536
|
+
- Can host and connect via CLI
|
|
537
|
+
- Two emoemu instances can play together
|
|
538
|
+
- Status bar shows netplay info
|
|
539
|
+
|
|
540
|
+
### Milestone 7: Testing & Polish (Week 7-8)
|
|
541
|
+
|
|
542
|
+
**Goal**: Comprehensive testing and edge case handling.
|
|
543
|
+
|
|
544
|
+
**Tasks**:
|
|
545
|
+
1. Unit tests
|
|
546
|
+
- Protocol encoding/decoding
|
|
547
|
+
- Frame buffer operations
|
|
548
|
+
- Sync manager logic
|
|
549
|
+
2. Integration tests
|
|
550
|
+
- Server/client handshake
|
|
551
|
+
- Input exchange
|
|
552
|
+
- Rollback scenarios
|
|
553
|
+
3. Manual testing
|
|
554
|
+
- Various cores (SNES, Genesis, GBA)
|
|
555
|
+
- Different latency conditions
|
|
556
|
+
- Edge cases (mid-game connect, disconnect)
|
|
557
|
+
4. Performance optimization
|
|
558
|
+
- Profile rollback performance
|
|
559
|
+
- Optimize state serialization
|
|
560
|
+
- Reduce memory allocations
|
|
561
|
+
5. Documentation
|
|
562
|
+
- Update CLAUDE.md with netplay section
|
|
563
|
+
- Add netplay usage examples
|
|
564
|
+
- Document protocol compatibility notes
|
|
565
|
+
|
|
566
|
+
**Deliverable**: Production-ready netplay support.
|
|
567
|
+
|
|
568
|
+
**Acceptance Criteria**:
|
|
569
|
+
- All tests pass
|
|
570
|
+
- Playable with 100ms+ latency
|
|
571
|
+
- No memory leaks during extended sessions
|
|
572
|
+
|
|
573
|
+
## Protocol Details
|
|
574
|
+
|
|
575
|
+
### Connection Header
|
|
576
|
+
|
|
577
|
+
First 4 bytes exchanged by both parties:
|
|
578
|
+
|
|
579
|
+
```
|
|
580
|
+
Offset Size Description
|
|
581
|
+
0 4 Magic: "RANP" (0x52414E50) - RetroArch Netplay
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Handshake Sequence
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
Client Server
|
|
588
|
+
| |
|
|
589
|
+
|-------- HEADER (RANP) ------->|
|
|
590
|
+
|<------- HEADER (RANP) --------|
|
|
591
|
+
| |
|
|
592
|
+
|-------- NICK (nickname) ----->|
|
|
593
|
+
|<------- NICK (nickname) ------|
|
|
594
|
+
| |
|
|
595
|
+
|-------- PASSWORD (hash) ----->| (if required)
|
|
596
|
+
| |
|
|
597
|
+
|<------- INFO (core info) -----|
|
|
598
|
+
|-------- INFO (core info) ---->|
|
|
599
|
+
| |
|
|
600
|
+
|<------- SYNC (state) ---------|
|
|
601
|
+
| |
|
|
602
|
+
|======= GAMEPLAY LOOP =========|
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### INPUT Command Format
|
|
606
|
+
|
|
607
|
+
```
|
|
608
|
+
Offset Size Description
|
|
609
|
+
0 4 Frame number (uint32, network byte order)
|
|
610
|
+
4 4 Client ID and flags:
|
|
611
|
+
- Bits 0-30: Client number
|
|
612
|
+
- Bit 31: Is server data flag
|
|
613
|
+
8 4 Joypad input (RETRO_DEVICE_JOYPAD bitmask)
|
|
614
|
+
12 4 Analog left (optional, if device supports)
|
|
615
|
+
16 4 Analog right (optional, if device supports)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### INFO Command Format
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
Offset Size Description
|
|
622
|
+
0 32 Core name (null-terminated string)
|
|
623
|
+
32 32 Core version (null-terminated string)
|
|
624
|
+
64 4 Content CRC32 (uint32, network byte order)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### SYNC Command Format
|
|
628
|
+
|
|
629
|
+
```
|
|
630
|
+
Offset Size Description
|
|
631
|
+
0 4 Frame number (uint32)
|
|
632
|
+
4 4 Flags:
|
|
633
|
+
- Bit 0: Paused
|
|
634
|
+
- Bits 1-31: Connected players bitmap
|
|
635
|
+
8 4 Flip frame (for player swap)
|
|
636
|
+
12 64 Controller devices (uint32[16])
|
|
637
|
+
76 32 Client nickname
|
|
638
|
+
108 var Serialized SRAM/state
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
## Risk Assessment
|
|
642
|
+
|
|
643
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
644
|
+
|------|------------|--------|------------|
|
|
645
|
+
| Protocol incompatibility with RetroArch | Medium | High | Test against multiple RA versions; document known working versions |
|
|
646
|
+
| Rollback performance too slow | Medium | Medium | Profile early; optimize state serialization; limit rollback depth |
|
|
647
|
+
| State size too large for some cores | Low | Medium | Implement compression; warn users about memory usage |
|
|
648
|
+
| Desync issues | High | Medium | Comprehensive CRC checking; detailed logging; state dumps for debugging |
|
|
649
|
+
| Network jitter causing poor experience | Medium | Medium | Tunable input delay; clear latency indicators |
|
|
650
|
+
|
|
651
|
+
## Future Enhancements (Post-MVP)
|
|
652
|
+
|
|
653
|
+
1. **Lobby Server Integration**: Announce sessions to libretro lobby
|
|
654
|
+
2. **NAT Traversal**: UPnP port forwarding, STUN/TURN
|
|
655
|
+
3. **Spectator Chat**: Text chat during spectating
|
|
656
|
+
4. **Input Display**: Show inputs on screen for spectators
|
|
657
|
+
5. **Replay Recording**: Save netplay sessions for replay
|
|
658
|
+
6. **Native Core Support**: Extend to native NES core
|
|
659
|
+
|
|
660
|
+
## References
|
|
661
|
+
|
|
662
|
+
- [RetroArch Netplay Documentation](https://docs.libretro.com/development/retroarch/netplay/)
|
|
663
|
+
- [RetroArch Netplay Source Code](https://github.com/libretro/RetroArch/tree/master/network/netplay)
|
|
664
|
+
- [Netplay FAQ](https://docs.libretro.com/guides/netplay-faq/)
|
|
665
|
+
- [netplay_private.h](https://github.com/libretro/RetroArch/blob/master/network/netplay/netplay_private.h)
|