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,536 @@
1
+ /**
2
+ * TCP Connection Wrapper for Netplay
3
+ *
4
+ * Handles buffered reads, command parsing, and connection state management.
5
+ */
6
+
7
+ import { Socket, createConnection } from 'net';
8
+ import { EventEmitter } from 'events';
9
+ import {
10
+ ConnectionState,
11
+ DEFAULT_PORT,
12
+ TCP_KEEPALIVE_MS,
13
+ CONNECTION_HEADER_SIZE,
14
+ CONNECTION_MAGIC_SIZE,
15
+ CONNECTION_MAGIC,
16
+ TIMEOUT_CLEANUP_DELAY_MS,
17
+ HEX_PREVIEW_LENGTH,
18
+ HEX_RADIX,
19
+ HEX_PADDING_WIDTH_32,
20
+ RECEIVE_BUFFER_INITIAL_SIZE,
21
+ RECEIVE_BUFFER_GROWTH_FACTOR,
22
+ NetplayError,
23
+ type RawCommand,
24
+ type ParsedCommand,
25
+ type ClientInfo,
26
+ } from '..';
27
+ import { netplayLogger } from '../netplayLogger';
28
+ import {
29
+ decodeCommand,
30
+ parseCommand,
31
+ createConnectionHeader,
32
+ parseConnectionHeader,
33
+ type ConnectionHeader,
34
+ } from '../protocol';
35
+
36
+ /** Connection event types */
37
+ interface ConnectionEvents {
38
+ command: (command: ParsedCommand) => void;
39
+ rawCommand: (command: RawCommand) => void;
40
+ connected: () => void;
41
+ disconnected: (reason: string) => void;
42
+ error: (error: Error) => void;
43
+ }
44
+
45
+ /**
46
+ * NetplayConnection wraps a TCP socket with buffered command parsing.
47
+ */
48
+ export class NetplayConnection extends EventEmitter {
49
+ private socket: Socket | null = null;
50
+ /** Pre-allocated receive buffer (grows as needed, avoids per-packet allocation) */
51
+ private receiveBuffer: Buffer = Buffer.alloc(RECEIVE_BUFFER_INITIAL_SIZE);
52
+ /** Number of valid bytes currently in receiveBuffer */
53
+ private receiveBufferLength: number = 0;
54
+ private _state: ConnectionState = ConnectionState.DISCONNECTED;
55
+ private _id: number = 0;
56
+ private _nickname: string = '';
57
+ private _playerNumber: number = -1;
58
+ private _spectating: boolean = false;
59
+ private _latency: number = 0;
60
+ private _lastReceivedFrame: number = 0;
61
+ private _devices: number[] = [];
62
+ private _address: string = '';
63
+ private _port: number = 0;
64
+ private _draining: boolean = false;
65
+
66
+ /** Unique client ID */
67
+ get id(): number {
68
+ return this._id;
69
+ }
70
+ set id(value: number) {
71
+ this._id = value;
72
+ }
73
+
74
+ /** Client nickname */
75
+ get nickname(): string {
76
+ return this._nickname;
77
+ }
78
+ set nickname(value: string) {
79
+ this._nickname = value;
80
+ }
81
+
82
+ /** Player number (-1 if spectating) */
83
+ get playerNumber(): number {
84
+ return this._playerNumber;
85
+ }
86
+ set playerNumber(value: number) {
87
+ this._playerNumber = value;
88
+ }
89
+
90
+ /** Is this connection spectating? */
91
+ get spectating(): boolean {
92
+ return this._spectating;
93
+ }
94
+ set spectating(value: boolean) {
95
+ this._spectating = value;
96
+ }
97
+
98
+ /** Estimated latency in ms */
99
+ get latency(): number {
100
+ return this._latency;
101
+ }
102
+ set latency(value: number) {
103
+ this._latency = value;
104
+ }
105
+
106
+ /** Last frame received from this connection */
107
+ get lastReceivedFrame(): number {
108
+ return this._lastReceivedFrame;
109
+ }
110
+ set lastReceivedFrame(value: number) {
111
+ this._lastReceivedFrame = value;
112
+ }
113
+
114
+ /** Input devices */
115
+ get devices(): number[] {
116
+ return this._devices;
117
+ }
118
+ set devices(value: number[]) {
119
+ this._devices = value;
120
+ }
121
+
122
+ /** Remote address */
123
+ get address(): string {
124
+ return this._address;
125
+ }
126
+
127
+ /** Remote port */
128
+ get port(): number {
129
+ return this._port;
130
+ }
131
+
132
+ /** Current connection state */
133
+ get state(): ConnectionState {
134
+ return this._state;
135
+ }
136
+
137
+ /** Is the connection currently open? */
138
+ get isConnected(): boolean {
139
+ return (
140
+ this._state !== ConnectionState.DISCONNECTED && this.socket !== null && !this.socket.destroyed
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Create a connection from an existing socket (server accepting a client).
146
+ */
147
+ static fromSocket(socket: Socket, clientId: number): NetplayConnection {
148
+ const conn = new NetplayConnection();
149
+ conn.socket = socket;
150
+ conn._id = clientId;
151
+ conn._state = ConnectionState.CONNECTED;
152
+
153
+ const addr = socket.remoteAddress ?? 'unknown';
154
+ const port = socket.remotePort ?? 0;
155
+ conn._address = addr;
156
+ conn._port = port;
157
+
158
+ conn.setupSocketHandlers();
159
+ return conn;
160
+ }
161
+
162
+ /**
163
+ * Connect to a remote server.
164
+ */
165
+ async connect(host: string, port: number = DEFAULT_PORT): Promise<void> {
166
+ if (this.socket) {
167
+ throw new NetplayError('ALREADY_CONNECTED');
168
+ }
169
+
170
+ return new Promise((resolve, reject) => {
171
+ this._address = host;
172
+ this._port = port;
173
+
174
+ this.socket = createConnection({ host, port }, () => {
175
+ this._state = ConnectionState.CONNECTED;
176
+ this.setupSocketHandlers();
177
+ this.emit('connected');
178
+ resolve();
179
+ });
180
+
181
+ this.socket.once('error', (err) => {
182
+ this._state = ConnectionState.DISCONNECTED;
183
+ reject(err);
184
+ });
185
+
186
+ // Set socket options
187
+ this.socket.setNoDelay(true);
188
+ this.socket.setKeepAlive(true, TCP_KEEPALIVE_MS);
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Close the connection.
194
+ */
195
+ close(reason: string = 'closed'): void {
196
+ // Log any pending data in receive buffer before closing
197
+ const bufferPreviewLength = HEX_PREVIEW_LENGTH * 2; // 64 bytes
198
+ if (this.receiveBufferLength > 0) {
199
+ const bufferPreview = this.receiveBuffer.subarray(0, Math.min(bufferPreviewLength, this.receiveBufferLength)).toString('hex');
200
+ netplayLogger.debug('CONNECTION', `Connection closing with ${this.receiveBufferLength} bytes in buffer`, {
201
+ reason,
202
+ bufferedBytes: this.receiveBufferLength,
203
+ bufferHex: bufferPreview + (this.receiveBufferLength > bufferPreviewLength ? '...' : ''),
204
+ });
205
+ } else {
206
+ netplayLogger.debug('CONNECTION', `Connection closing`, { reason, bufferedBytes: 0 });
207
+ }
208
+
209
+ if (this.socket) {
210
+ this.socket.destroy();
211
+ this.socket = null;
212
+ }
213
+ if (this._state !== ConnectionState.DISCONNECTED) {
214
+ this._state = ConnectionState.DISCONNECTED;
215
+ this.emit('disconnected', reason);
216
+ }
217
+ // Reset buffer length instead of reallocating (keeps the buffer for potential reuse)
218
+ this.receiveBufferLength = 0;
219
+ }
220
+
221
+ /** Is the socket buffer full and draining? */
222
+ get draining(): boolean {
223
+ return this._draining;
224
+ }
225
+
226
+ /**
227
+ * Send raw data over the connection.
228
+ * Returns false if socket is unavailable or buffer is full.
229
+ */
230
+ send(data: Buffer): boolean {
231
+ if (!this.socket || this.socket.destroyed) {
232
+ netplayLogger.debug('CONNECTION', `Send failed - socket not available`, {
233
+ socketExists: !!this.socket,
234
+ socketDestroyed: this.socket?.destroyed ?? 'N/A',
235
+ dataSize: data.length,
236
+ });
237
+ return false;
238
+ }
239
+
240
+ // If already draining, skip non-critical data to avoid flooding
241
+ if (this._draining) {
242
+ return false;
243
+ }
244
+
245
+ const success = this.socket.write(data);
246
+ if (!success) {
247
+ this._draining = true;
248
+ netplayLogger.debug('CONNECTION', `Socket buffer full, entering drain mode`, {
249
+ dataSize: data.length,
250
+ });
251
+ }
252
+ return success;
253
+ }
254
+
255
+ /**
256
+ * Send the connection header.
257
+ * @param nickname The nickname (for logging only, not sent in header)
258
+ * @param isServer Whether this is a server sending to a client
259
+ * @param salt Password salt (server only, 0 = no password required)
260
+ */
261
+ sendHeader(nickname: string = 'emoemu', isServer: boolean = false, salt: number = 0): boolean {
262
+ const header = createConnectionHeader({ isServer, salt });
263
+ netplayLogger.debug('CONNECTION', `Sending connection header to ${this._address}`, {
264
+ headerHex: header.toString('hex'),
265
+ headerSize: header.length,
266
+ nickname,
267
+ });
268
+ return this.send(header);
269
+ }
270
+
271
+ /**
272
+ * Wait for and parse the connection header from remote.
273
+ * Returns the parsed header on success, or null on failure.
274
+ */
275
+ async waitForHeader(timeoutMs: number = 5000): Promise<ConnectionHeader | null> {
276
+ return new Promise((resolve) => {
277
+ let resolved = false;
278
+
279
+ const timeout = setTimeout(() => {
280
+ if (!resolved) {
281
+ resolved = true;
282
+ netplayLogger.debug('CONNECTION', `Header timeout waiting for ${this._address}`, {
283
+ bufferSize: this.receiveBufferLength,
284
+ });
285
+ resolve(null);
286
+ }
287
+ }, timeoutMs);
288
+
289
+ const checkHeader = (): void => {
290
+ if (resolved) {
291
+ return;
292
+ }
293
+
294
+ // Need at least the minimum header size to start checking
295
+ if (this.receiveBufferLength < CONNECTION_HEADER_SIZE) {
296
+ return;
297
+ }
298
+
299
+ // Try to parse the full header (includes variable nickname)
300
+ // Create a view of only the valid data for parsing
301
+ const validData = this.receiveBuffer.subarray(0, this.receiveBufferLength);
302
+ const result = parseConnectionHeader(validData);
303
+ if (result) {
304
+ resolved = true;
305
+ clearTimeout(timeout);
306
+ // Shift consumed bytes out of buffer
307
+ this.consumeBuffer(result.bytesConsumed);
308
+ this._state = ConnectionState.HANDSHAKING;
309
+
310
+ netplayLogger.debug('CONNECTION', `Received valid header from ${this._address}`, {
311
+ nickname: result.header.nickname,
312
+ platformMagic: result.header.platformMagic.toString(HEX_RADIX),
313
+ compression: result.header.compression,
314
+ bytesConsumed: result.bytesConsumed,
315
+ });
316
+
317
+ resolve(result.header);
318
+ } else if (this.receiveBufferLength >= CONNECTION_MAGIC_SIZE) {
319
+ // Check if magic is wrong (invalid connection)
320
+ const magic = this.receiveBuffer.readUInt32BE(0);
321
+ if (magic !== CONNECTION_MAGIC) {
322
+ resolved = true;
323
+ clearTimeout(timeout);
324
+ netplayLogger.debug('CONNECTION', `Invalid magic from ${this._address}`, {
325
+ expectedMagic: CONNECTION_MAGIC.toString(HEX_RADIX),
326
+ receivedMagic: magic.toString(HEX_RADIX).padStart(HEX_PADDING_WIDTH_32, '0'),
327
+ });
328
+ resolve(null);
329
+ }
330
+ // Otherwise, magic is valid but we don't have enough data yet - wait for more
331
+ }
332
+ };
333
+
334
+ // Check if we already have the header
335
+ checkHeader();
336
+
337
+ // Listen for more data
338
+ const onData = (): void => {
339
+ checkHeader();
340
+ };
341
+
342
+ this.socket?.on('data', onData);
343
+
344
+ // Cleanup after resolution
345
+ setTimeout(() => {
346
+ this.socket?.off('data', onData);
347
+ }, timeoutMs + TIMEOUT_CLEANUP_DELAY_MS);
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Update connection state.
353
+ */
354
+ setState(state: ConnectionState): void {
355
+ this._state = state;
356
+ }
357
+
358
+ /**
359
+ * Get client info snapshot.
360
+ */
361
+ getClientInfo(): ClientInfo {
362
+ return {
363
+ id: this._id,
364
+ nickname: this._nickname,
365
+ address: this._address,
366
+ port: this._port,
367
+ state: this._state,
368
+ playerNumber: this._playerNumber,
369
+ spectating: this._spectating,
370
+ latency: this._latency,
371
+ lastReceivedFrame: this._lastReceivedFrame,
372
+ devices: [...this._devices],
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Process any complete commands in the receive buffer.
378
+ */
379
+ processBuffer(): void {
380
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
381
+ while (true) {
382
+ // Create a view of only the valid data for parsing
383
+ const validData = this.receiveBuffer.subarray(0, this.receiveBufferLength);
384
+ const result = decodeCommand(validData);
385
+ if (!result) {
386
+ break;
387
+ }
388
+
389
+ const { command, bytesConsumed } = result;
390
+ // Shift consumed bytes out of buffer
391
+ this.consumeBuffer(bytesConsumed);
392
+
393
+ // Emit raw command
394
+ this.emit('rawCommand', command);
395
+
396
+ // Parse and emit typed command
397
+ try {
398
+ const parsed = parseCommand(command);
399
+ this.emit('command', parsed);
400
+ } catch (err) {
401
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
402
+ }
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Append data to the receive buffer, growing if necessary.
408
+ * This avoids Buffer.concat allocation on every packet.
409
+ */
410
+ private appendToBuffer(data: Buffer): void {
411
+ const requiredSize = this.receiveBufferLength + data.length;
412
+
413
+ // Grow buffer if needed
414
+ if (requiredSize > this.receiveBuffer.length) {
415
+ let newSize = this.receiveBuffer.length;
416
+ while (newSize < requiredSize) {
417
+ newSize *= RECEIVE_BUFFER_GROWTH_FACTOR;
418
+ }
419
+ const newBuffer = Buffer.alloc(newSize);
420
+ // Copy existing valid data
421
+ this.receiveBuffer.copy(newBuffer, 0, 0, this.receiveBufferLength);
422
+ this.receiveBuffer = newBuffer;
423
+ }
424
+
425
+ // Append new data
426
+ data.copy(this.receiveBuffer, this.receiveBufferLength);
427
+ this.receiveBufferLength += data.length;
428
+ }
429
+
430
+ /**
431
+ * Remove consumed bytes from the front of the receive buffer.
432
+ * Uses in-place copy to avoid allocation.
433
+ */
434
+ private consumeBuffer(bytesConsumed: number): void {
435
+ if (bytesConsumed >= this.receiveBufferLength) {
436
+ // All data consumed, just reset length
437
+ this.receiveBufferLength = 0;
438
+ } else {
439
+ // Shift remaining data to front
440
+ this.receiveBuffer.copy(
441
+ this.receiveBuffer,
442
+ 0,
443
+ bytesConsumed,
444
+ this.receiveBufferLength
445
+ );
446
+ this.receiveBufferLength -= bytesConsumed;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Set up socket event handlers.
452
+ */
453
+ private setupSocketHandlers(): void {
454
+ if (!this.socket) {
455
+ return;
456
+ }
457
+
458
+ this.socket.on('data', (data: Buffer) => {
459
+ // Log raw data received for debugging
460
+ const hexPreview = data.subarray(0, Math.min(HEX_PREVIEW_LENGTH, data.length)).toString('hex');
461
+ netplayLogger.debug('CONNECTION', `Received ${data.length} bytes from ${this._address}`, {
462
+ bytesReceived: data.length,
463
+ hexPreview: hexPreview + (data.length > HEX_PREVIEW_LENGTH ? '...' : ''),
464
+ bufferSizeBefore: this.receiveBufferLength,
465
+ });
466
+
467
+ // Append to receive buffer (grows as needed, avoids per-packet allocation)
468
+ this.appendToBuffer(data);
469
+ // Process any complete commands
470
+ this.processBuffer();
471
+ });
472
+
473
+ this.socket.on('close', () => {
474
+ this.close('connection closed');
475
+ });
476
+
477
+ this.socket.on('error', (err) => {
478
+ // Capture additional error details for debugging
479
+ const errWithCode = err as NodeJS.ErrnoException;
480
+ const errDetails = {
481
+ message: err.message,
482
+ code: errWithCode.code,
483
+ syscall: errWithCode.syscall,
484
+ errno: errWithCode.errno,
485
+ };
486
+ netplayLogger.debug('CONNECTION', `Socket error from ${this._address}`, errDetails);
487
+ this.emit('error', err);
488
+ this.close(`error: ${err.message}`);
489
+ });
490
+
491
+ this.socket.on('timeout', () => {
492
+ this.close('timeout');
493
+ });
494
+
495
+ this.socket.on('drain', () => {
496
+ if (this._draining) {
497
+ this._draining = false;
498
+ netplayLogger.debug('CONNECTION', `Socket drained, resuming sends to ${this._address}`);
499
+ }
500
+ });
501
+ }
502
+
503
+ // Type-safe event emitter methods
504
+ override on<K extends keyof ConnectionEvents>(
505
+ event: K,
506
+ listener: ConnectionEvents[K]
507
+ ): this {
508
+ return super.on(event, listener);
509
+ }
510
+
511
+ override off<K extends keyof ConnectionEvents>(
512
+ event: K,
513
+ listener: ConnectionEvents[K]
514
+ ): this {
515
+ return super.off(event, listener);
516
+ }
517
+
518
+ override emit<K extends keyof ConnectionEvents>(
519
+ event: K,
520
+ ...args: Parameters<ConnectionEvents[K]>
521
+ ): boolean {
522
+ return super.emit(event, ...args);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Create a connection and connect to a server.
528
+ */
529
+ export const createNetplayConnection = async (
530
+ host: string,
531
+ port: number = DEFAULT_PORT
532
+ ): Promise<NetplayConnection> => {
533
+ const conn = new NetplayConnection();
534
+ await conn.connect(host, port);
535
+ return conn;
536
+ };
@@ -0,0 +1,41 @@
1
+ import { MAX_NICK_LEN, TIMING, UINT32_SIZE } from '..';
2
+
3
+ /** Discovery query magic: "RANQ" (RetroArch Netplay Query) */
4
+ export const DISCOVERY_QUERY_MAGIC = 0x52414e51;
5
+
6
+ /** Discovery response magic: "RANS" (RetroArch Netplay Server) */
7
+ export const DISCOVERY_RESPONSE_MAGIC = 0x52414e53;
8
+
9
+ /** Size of a query packet (just the magic header) */
10
+ export const QUERY_PACKET_SIZE = 4;
11
+
12
+ /** String length constants from RetroArch */
13
+ export const NETPLAY_HOST_STR_LEN = 32;
14
+ export const NETPLAY_HOST_LONGSTR_LEN = 256;
15
+
16
+ /** Microseconds to milliseconds conversion */
17
+ export const MS_PER_USEC = 1000;
18
+
19
+ /** Byte mask for broadcast address calculation */
20
+ export const BYTE_MASK = 255;
21
+
22
+ /** Broadcast interval in milliseconds (5 seconds) */
23
+ export const BROADCAST_INTERVAL_MS = TIMING.ANNOUNCE_AFTER_USEC / MS_PER_USEC;
24
+
25
+ /** Password bitmask values */
26
+ export const PASSWORD_FLAG = 1;
27
+ export const SPECTATE_PASSWORD_FLAG = 2;
28
+
29
+ /**
30
+ * Discovery packet structure field count (header, content_crc, port, has_password)
31
+ */
32
+ export const DISCOVERY_HEADER_FIELDS = 4;
33
+
34
+ export const DISCOVERY_PACKET_SIZE = (UINT32_SIZE * DISCOVERY_HEADER_FIELDS) +
35
+ MAX_NICK_LEN + // nick[32]
36
+ NETPLAY_HOST_STR_LEN + // frontend[32]
37
+ NETPLAY_HOST_STR_LEN + // core[32]
38
+ NETPLAY_HOST_STR_LEN + // core_version[32]
39
+ NETPLAY_HOST_STR_LEN + // retroarch_version[32]
40
+ NETPLAY_HOST_LONGSTR_LEN + // content[256]
41
+ NETPLAY_HOST_LONGSTR_LEN; // subsystem_name[256]