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,286 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { FrameBuffer, createFrameBuffer } from '.';
3
+ import { crc32 } from '../crc32';
4
+ import { DEFAULT_FRAME_BUFFER_SIZE, MAX_INPUT_DEVICES } from '..';
5
+
6
+ describe('FrameBuffer', () => {
7
+ let buffer: FrameBuffer;
8
+
9
+ beforeEach(() => {
10
+ buffer = createFrameBuffer(10); // Small buffer for testing
11
+ });
12
+
13
+ describe('initialization', () => {
14
+ it('should create buffer with specified capacity', () => {
15
+ expect(buffer.capacity).toBe(10);
16
+ });
17
+
18
+ it('should start empty', () => {
19
+ expect(buffer.size).toBe(0);
20
+ expect(buffer.newestFrame).toBe(-1);
21
+ });
22
+
23
+ it('should use default capacity if not specified', () => {
24
+ const defaultBuffer = createFrameBuffer();
25
+ expect(defaultBuffer.capacity).toBe(DEFAULT_FRAME_BUFFER_SIZE);
26
+ });
27
+ });
28
+
29
+ describe('advance', () => {
30
+ it('should advance to first frame', () => {
31
+ const frame = buffer.advance();
32
+ expect(frame.frameNumber).toBe(0);
33
+ expect(buffer.newestFrame).toBe(0);
34
+ expect(buffer.size).toBe(1);
35
+ });
36
+
37
+ it('should advance multiple frames', () => {
38
+ for (let i = 0; i < 5; i++) {
39
+ buffer.advance();
40
+ }
41
+ expect(buffer.newestFrame).toBe(4);
42
+ expect(buffer.oldestFrame).toBe(0);
43
+ expect(buffer.size).toBe(5);
44
+ });
45
+
46
+ it('should wrap around when exceeding capacity', () => {
47
+ for (let i = 0; i < 15; i++) {
48
+ buffer.advance();
49
+ }
50
+ expect(buffer.newestFrame).toBe(14);
51
+ expect(buffer.oldestFrame).toBe(5);
52
+ expect(buffer.size).toBe(10);
53
+ });
54
+
55
+ it('should reset frame state on reuse', () => {
56
+ const frame1 = buffer.advance();
57
+ frame1.serializedState = Buffer.from([1, 2, 3]);
58
+ frame1.crc = 12345;
59
+
60
+ // Advance past capacity to reuse slot
61
+ for (let i = 0; i < 10; i++) {
62
+ buffer.advance();
63
+ }
64
+
65
+ // Frame 10 should be in slot 0, which was frame 0
66
+ const frame10 = buffer.get(10);
67
+ expect(frame10).not.toBeNull();
68
+ expect(frame10!.serializedState).toBeNull();
69
+ expect(frame10!.crc).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe('get', () => {
74
+ it('should return null for frame not in buffer', () => {
75
+ expect(buffer.get(0)).toBeNull();
76
+ expect(buffer.get(100)).toBeNull();
77
+ });
78
+
79
+ it('should return frame by number', () => {
80
+ buffer.advance();
81
+ buffer.advance();
82
+ const frame = buffer.get(1);
83
+ expect(frame).not.toBeNull();
84
+ expect(frame!.frameNumber).toBe(1);
85
+ });
86
+
87
+ it('should return null for expired frame', () => {
88
+ for (let i = 0; i < 15; i++) {
89
+ buffer.advance();
90
+ }
91
+ expect(buffer.get(0)).toBeNull();
92
+ expect(buffer.get(4)).toBeNull();
93
+ expect(buffer.get(5)).not.toBeNull();
94
+ });
95
+ });
96
+
97
+ describe('getCurrent', () => {
98
+ it('should return null when empty', () => {
99
+ expect(buffer.getCurrent()).toBeNull();
100
+ });
101
+
102
+ it('should return newest frame', () => {
103
+ buffer.advance();
104
+ buffer.advance();
105
+ buffer.advance();
106
+ const current = buffer.getCurrent();
107
+ expect(current).not.toBeNull();
108
+ expect(current!.frameNumber).toBe(2);
109
+ });
110
+ });
111
+
112
+ describe('hasFrame', () => {
113
+ it('should return false for empty buffer', () => {
114
+ expect(buffer.hasFrame(0)).toBe(false);
115
+ });
116
+
117
+ it('should return true for frames in range', () => {
118
+ for (let i = 0; i < 5; i++) {
119
+ buffer.advance();
120
+ }
121
+ expect(buffer.hasFrame(0)).toBe(true);
122
+ expect(buffer.hasFrame(4)).toBe(true);
123
+ });
124
+
125
+ it('should return false for frames outside range', () => {
126
+ for (let i = 0; i < 5; i++) {
127
+ buffer.advance();
128
+ }
129
+ expect(buffer.hasFrame(5)).toBe(false);
130
+ expect(buffer.hasFrame(-1)).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe('state management', () => {
135
+ it('should set and get state', () => {
136
+ buffer.advance();
137
+ const state = Buffer.from([1, 2, 3, 4, 5]);
138
+ expect(buffer.setState(0, state)).toBe(true);
139
+ expect(buffer.getState(0)).toEqual(state);
140
+ });
141
+
142
+ it('should compute CRC when setting state', () => {
143
+ buffer.advance();
144
+ const state = Buffer.from('test state');
145
+ buffer.setState(0, state);
146
+ const crc = buffer.getCrc(0);
147
+ expect(crc).not.toBeNull();
148
+ expect(crc).toBe(crc32(state));
149
+ });
150
+
151
+ it('should return false when setting state for invalid frame', () => {
152
+ expect(buffer.setState(0, Buffer.alloc(0))).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe('local input', () => {
157
+ it('should set local input for current frame', () => {
158
+ buffer.advance();
159
+ const input = [0xff, 0x1234, 0x5678];
160
+ expect(buffer.setLocalInput(input)).toBe(true);
161
+
162
+ const frame = buffer.getCurrent();
163
+ expect(frame!.localInput[0]).toBe(0xff);
164
+ expect(frame!.localInput[1]).toBe(0x1234);
165
+ expect(frame!.localInput[2]).toBe(0x5678);
166
+ });
167
+
168
+ it('should set input for specific device', () => {
169
+ buffer.advance();
170
+ expect(buffer.setLocalInputForDevice(1, 0xabcd, 0x1111, 0x2222)).toBe(true);
171
+
172
+ const frame = buffer.getCurrent();
173
+ // Device 1 starts at index 3 (device 0 uses indices 0-2)
174
+ expect(frame!.localInput[3]).toBe(0xabcd);
175
+ expect(frame!.localInput[4]).toBe(0x1111);
176
+ expect(frame!.localInput[5]).toBe(0x2222);
177
+ });
178
+
179
+ it('should reject device index out of range', () => {
180
+ buffer.advance();
181
+ expect(buffer.setLocalInputForDevice(MAX_INPUT_DEVICES, 0xff)).toBe(false);
182
+ });
183
+ });
184
+
185
+ describe('remote input', () => {
186
+ it('should set and get remote input', () => {
187
+ buffer.advance();
188
+ const input = [0xaa, 0xbb, 0xcc];
189
+ buffer.setRemoteInput(0, 1, input, true);
190
+
191
+ expect(buffer.getRemoteInput(0, 1)).toEqual(input);
192
+ expect(buffer.isRemoteInputReal(0, 1)).toBe(true);
193
+ });
194
+
195
+ it('should track simulated vs real input', () => {
196
+ buffer.advance();
197
+ buffer.setRemoteInput(0, 1, [1, 2, 3], false);
198
+ expect(buffer.isRemoteInputReal(0, 1)).toBe(false);
199
+
200
+ buffer.setRemoteInput(0, 1, [4, 5, 6], true);
201
+ expect(buffer.isRemoteInputReal(0, 1)).toBe(true);
202
+ });
203
+
204
+ it('should return null for unknown client', () => {
205
+ buffer.advance();
206
+ expect(buffer.getRemoteInput(0, 99)).toBeNull();
207
+ });
208
+ });
209
+
210
+ describe('sync frame finding', () => {
211
+ it('should find sync frame with all real input', () => {
212
+ for (let i = 0; i < 5; i++) {
213
+ buffer.advance();
214
+ buffer.setRemoteInput(i, 1, [i], true);
215
+ buffer.setRemoteInput(i, 2, [i], true);
216
+ }
217
+
218
+ const syncFrame = buffer.findSyncFrame([1, 2]);
219
+ expect(syncFrame).toBe(4);
220
+ });
221
+
222
+ it('should find last frame before simulated input', () => {
223
+ for (let i = 0; i < 5; i++) {
224
+ buffer.advance();
225
+ buffer.setRemoteInput(i, 1, [i], true);
226
+ buffer.setRemoteInput(i, 2, [i], i < 3); // Only real for frames 0-2
227
+ }
228
+
229
+ const syncFrame = buffer.findSyncFrame([1, 2]);
230
+ expect(syncFrame).toBe(2);
231
+ });
232
+
233
+ it('should return null if no synced frames', () => {
234
+ for (let i = 0; i < 3; i++) {
235
+ buffer.advance();
236
+ buffer.setRemoteInput(i, 1, [i], false);
237
+ }
238
+
239
+ const syncFrame = buffer.findSyncFrame([1]);
240
+ expect(syncFrame).toBeNull();
241
+ });
242
+ });
243
+
244
+ describe('unread frame finding', () => {
245
+ it('should find first frame with simulated input', () => {
246
+ for (let i = 0; i < 5; i++) {
247
+ buffer.advance();
248
+ buffer.setRemoteInput(i, 1, [i], i < 3);
249
+ }
250
+
251
+ const unreadFrame = buffer.findUnreadFrame([1]);
252
+ expect(unreadFrame).toBe(3);
253
+ });
254
+
255
+ it('should return null if all frames have real input', () => {
256
+ for (let i = 0; i < 5; i++) {
257
+ buffer.advance();
258
+ buffer.setRemoteInput(i, 1, [i], true);
259
+ }
260
+
261
+ const unreadFrame = buffer.findUnreadFrame([1]);
262
+ expect(unreadFrame).toBeNull();
263
+ });
264
+ });
265
+
266
+ describe('initializeAt', () => {
267
+ it('should initialize buffer at specific frame', () => {
268
+ const frame = buffer.initializeAt(1000);
269
+ expect(frame.frameNumber).toBe(1000);
270
+ expect(buffer.newestFrame).toBe(1000);
271
+ expect(buffer.oldestFrame).toBe(1000);
272
+ });
273
+ });
274
+
275
+ describe('clear', () => {
276
+ it('should reset buffer to empty state', () => {
277
+ for (let i = 0; i < 5; i++) {
278
+ buffer.advance();
279
+ }
280
+
281
+ buffer.clear();
282
+ expect(buffer.size).toBe(0);
283
+ expect(buffer.newestFrame).toBe(-1);
284
+ });
285
+ });
286
+ });
@@ -0,0 +1,7 @@
1
+ /** Number of input values per device (joypad, analogLeft, analogRight) */
2
+ export const INPUTS_PER_DEVICE = 3;
3
+
4
+ /** Index offsets for input values */
5
+ export const INPUT_JOYPAD = 0;
6
+ export const INPUT_ANALOG_LEFT = 1;
7
+ export const INPUT_ANALOG_RIGHT = 2;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Input Buffer for Netplay
3
+ *
4
+ * Manages input state per client, including:
5
+ * - Input delay queue for local input
6
+ * - Input prediction for remote clients
7
+ * - Frame-accurate input tracking
8
+ */
9
+
10
+ import { times } from 'remeda';
11
+ import { MAX_INPUT_DEVICES, MAX_INPUT_DELAY_FRAMES } from '..';
12
+
13
+ export * from './consts';
14
+ import { INPUTS_PER_DEVICE, INPUT_JOYPAD, INPUT_ANALOG_LEFT, INPUT_ANALOG_RIGHT } from './consts';
15
+
16
+ /**
17
+ * Create a zero-filled number array of the given size.
18
+ */
19
+ const createZeroArray = (size: number): number[] => times(size, () => 0);
20
+
21
+ /**
22
+ * Per-client input state tracking.
23
+ */
24
+ interface ClientInputState {
25
+ /** Client ID */
26
+ clientId: number;
27
+
28
+ /** Last known input values per device */
29
+ lastInput: number[];
30
+
31
+ /** Last frame we received real input for */
32
+ lastRealInputFrame: number;
33
+
34
+ /** Is this client local? */
35
+ isLocal: boolean;
36
+ }
37
+
38
+ /**
39
+ * Delayed input entry for local input delay queue.
40
+ */
41
+ interface DelayedInput {
42
+ /** Frame number this input is for */
43
+ frameNumber: number;
44
+
45
+ /** Input values */
46
+ input: number[];
47
+ }
48
+
49
+ /**
50
+ * InputBuffer manages input state for all clients in a netplay session.
51
+ */
52
+ export class InputBuffer {
53
+ private readonly clients: Map<number, ClientInputState> = new Map();
54
+ private readonly delayQueue: DelayedInput[] = [];
55
+ private _inputDelayFrames: number = 0;
56
+ private _localClientId: number = 0;
57
+
58
+ /** Current input delay in frames */
59
+ get inputDelayFrames(): number {
60
+ return this._inputDelayFrames;
61
+ }
62
+
63
+ /** Local client ID */
64
+ get localClientId(): number {
65
+ return this._localClientId;
66
+ }
67
+
68
+ /**
69
+ * Initialize the input buffer with local client ID and delay.
70
+ */
71
+ initialize(localClientId: number, inputDelayFrames: number = 0): void {
72
+ this._localClientId = localClientId;
73
+ this._inputDelayFrames = Math.min(inputDelayFrames, MAX_INPUT_DELAY_FRAMES);
74
+ this.clients.clear();
75
+ this.delayQueue.length = 0;
76
+
77
+ // Register local client
78
+ this.registerClient(localClientId, true);
79
+ }
80
+
81
+ /**
82
+ * Update the local client ID.
83
+ * Called when MODE command assigns our client number.
84
+ */
85
+ updateLocalClientId(newClientId: number): void {
86
+ const oldId = this._localClientId;
87
+ if (oldId === newClientId) {
88
+ return;
89
+ }
90
+
91
+ // Move client state from old ID to new ID
92
+ const oldState = this.clients.get(oldId);
93
+ if (oldState) {
94
+ this.clients.delete(oldId);
95
+ oldState.clientId = newClientId;
96
+ this.clients.set(newClientId, oldState);
97
+ }
98
+
99
+ this._localClientId = newClientId;
100
+ }
101
+
102
+ /**
103
+ * Register a client for input tracking.
104
+ */
105
+ registerClient(clientId: number, isLocal: boolean = false): void {
106
+ if (this.clients.has(clientId)) {
107
+ return;
108
+ }
109
+
110
+ this.clients.set(clientId, {
111
+ clientId,
112
+ lastInput: createZeroArray(MAX_INPUT_DEVICES * INPUTS_PER_DEVICE),
113
+ lastRealInputFrame: -1,
114
+ isLocal,
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Unregister a client.
120
+ */
121
+ unregisterClient(clientId: number): void {
122
+ this.clients.delete(clientId);
123
+ }
124
+
125
+ /**
126
+ * Get all registered client IDs.
127
+ */
128
+ getClientIds(): number[] {
129
+ return Array.from(this.clients.keys());
130
+ }
131
+
132
+ /**
133
+ * Get remote client IDs (excluding local).
134
+ */
135
+ getRemoteClientIds(): number[] {
136
+ return Array.from(this.clients.entries())
137
+ .filter(([_, state]) => !state.isLocal)
138
+ .map(([id]) => id);
139
+ }
140
+
141
+ /**
142
+ * Queue local input with delay.
143
+ * Returns the frame number this input will be applied to.
144
+ */
145
+ queueLocalInput(currentFrame: number, input: number[]): number {
146
+ const targetFrame = currentFrame + this._inputDelayFrames;
147
+
148
+ this.delayQueue.push({
149
+ frameNumber: targetFrame,
150
+ input: [...input],
151
+ });
152
+
153
+ // Update last known input for local client
154
+ const localState = this.clients.get(this._localClientId);
155
+ if (localState) {
156
+ const len = Math.min(input.length, localState.lastInput.length);
157
+ for (let i = 0; i < len; i++) {
158
+ localState.lastInput[i] = input[i];
159
+ }
160
+ }
161
+
162
+ return targetFrame;
163
+ }
164
+
165
+ /**
166
+ * Get local input for a specific frame from the delay queue.
167
+ * Returns null if not yet available.
168
+ */
169
+ getDelayedLocalInput(frameNumber: number): number[] | null {
170
+ const entry = this.delayQueue.find((e) => e.frameNumber === frameNumber);
171
+ return entry?.input ?? null;
172
+ }
173
+
174
+ /**
175
+ * Remove processed entries from the delay queue.
176
+ */
177
+ pruneDelayQueue(upToFrame: number): void {
178
+ while (this.delayQueue.length > 0 && this.delayQueue[0].frameNumber <= upToFrame) {
179
+ this.delayQueue.shift();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Record received input from a remote client.
185
+ */
186
+ recordRemoteInput(clientId: number, frameNumber: number, input: number[]): void {
187
+ let state = this.clients.get(clientId);
188
+ if (!state) {
189
+ // Auto-register unknown client
190
+ this.registerClient(clientId, false);
191
+ state = this.clients.get(clientId)!;
192
+ }
193
+
194
+ // Update last known input
195
+ const len = Math.min(input.length, state.lastInput.length);
196
+ for (let i = 0; i < len; i++) {
197
+ state.lastInput[i] = input[i];
198
+ }
199
+
200
+ // Update last real input frame
201
+ if (frameNumber > state.lastRealInputFrame) {
202
+ state.lastRealInputFrame = frameNumber;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Get input for a client at a specific frame.
208
+ * If real input isn't available, returns predicted input.
209
+ */
210
+ getInputForClient(
211
+ clientId: number,
212
+ _frameNumber: number,
213
+ realInputAvailable: boolean
214
+ ): { input: number[]; isReal: boolean } {
215
+ const state = this.clients.get(clientId);
216
+ if (!state) {
217
+ // Unknown client - return zeros
218
+ return {
219
+ input: createZeroArray(MAX_INPUT_DEVICES * INPUTS_PER_DEVICE),
220
+ isReal: false,
221
+ };
222
+ }
223
+
224
+ if (realInputAvailable) {
225
+ return {
226
+ input: [...state.lastInput],
227
+ isReal: true,
228
+ };
229
+ }
230
+
231
+ // Predict: repeat last known input
232
+ return {
233
+ input: this.predictInput(state),
234
+ isReal: false,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Predict input for a client based on last known state.
240
+ * Simple strategy: repeat last known input.
241
+ */
242
+ private predictInput(state: ClientInputState): number[] {
243
+ // Simple prediction: repeat last known input
244
+ return [...state.lastInput];
245
+ }
246
+
247
+ /**
248
+ * Get the last frame we received real input for a client.
249
+ */
250
+ getLastRealInputFrame(clientId: number): number {
251
+ return this.clients.get(clientId)?.lastRealInputFrame ?? -1;
252
+ }
253
+
254
+ /**
255
+ * Set input for a specific device.
256
+ * Convenience method for setting joypad + analog values.
257
+ */
258
+ setDeviceInput(
259
+ clientId: number,
260
+ deviceIndex: number,
261
+ joypad: number,
262
+ analogLeft: number = 0,
263
+ analogRight: number = 0
264
+ ): void {
265
+ const state = this.clients.get(clientId);
266
+ if (!state || deviceIndex >= MAX_INPUT_DEVICES) {
267
+ return;
268
+ }
269
+
270
+ const base = deviceIndex * INPUTS_PER_DEVICE;
271
+ state.lastInput[base + INPUT_JOYPAD] = joypad;
272
+ state.lastInput[base + INPUT_ANALOG_LEFT] = analogLeft;
273
+ state.lastInput[base + INPUT_ANALOG_RIGHT] = analogRight;
274
+ }
275
+
276
+ /**
277
+ * Get input for a specific device.
278
+ */
279
+ getDeviceInput(
280
+ clientId: number,
281
+ deviceIndex: number
282
+ ): { joypad: number; analogLeft: number; analogRight: number } | null {
283
+ const state = this.clients.get(clientId);
284
+ if (!state || deviceIndex >= MAX_INPUT_DEVICES) {
285
+ return null;
286
+ }
287
+
288
+ const base = deviceIndex * INPUTS_PER_DEVICE;
289
+ return {
290
+ joypad: state.lastInput[base + INPUT_JOYPAD],
291
+ analogLeft: state.lastInput[base + INPUT_ANALOG_LEFT],
292
+ analogRight: state.lastInput[base + INPUT_ANALOG_RIGHT],
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Merge input from multiple clients into a single input array.
298
+ * Each client controls specific device slots.
299
+ */
300
+ mergeInputs(
301
+ clientDeviceMap: Map<number, number[]>,
302
+ frameNumber: number,
303
+ realInputMap: Map<number, boolean>
304
+ ): { merged: number[]; allReal: boolean } {
305
+ const merged = createZeroArray(MAX_INPUT_DEVICES * INPUTS_PER_DEVICE);
306
+ let allReal = true;
307
+
308
+ for (const [clientId, deviceIndices] of clientDeviceMap) {
309
+ const isReal = realInputMap.get(clientId) ?? false;
310
+ const { input } = this.getInputForClient(clientId, frameNumber, isReal);
311
+
312
+ if (!isReal) {
313
+ allReal = false;
314
+ }
315
+
316
+ for (const deviceIndex of deviceIndices) {
317
+ if (deviceIndex >= MAX_INPUT_DEVICES) {
318
+ continue;
319
+ }
320
+
321
+ const srcBase = 0; // Assuming client's first device maps to their assigned slot
322
+ const dstBase = deviceIndex * INPUTS_PER_DEVICE;
323
+
324
+ merged[dstBase + INPUT_JOYPAD] = input[srcBase + INPUT_JOYPAD];
325
+ merged[dstBase + INPUT_ANALOG_LEFT] = input[srcBase + INPUT_ANALOG_LEFT];
326
+ merged[dstBase + INPUT_ANALOG_RIGHT] = input[srcBase + INPUT_ANALOG_RIGHT];
327
+ }
328
+ }
329
+
330
+ return { merged, allReal };
331
+ }
332
+
333
+ /**
334
+ * Clear all client state and queues.
335
+ */
336
+ clear(): void {
337
+ this.clients.clear();
338
+ this.delayQueue.length = 0;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Create a new input buffer.
344
+ */
345
+ export const createInputBuffer = (): InputBuffer => {
346
+ return new InputBuffer();
347
+ };