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,38 @@
1
+ /**
2
+ * Input System
3
+ *
4
+ * Handles keyboard and gamepad input for the emulator.
5
+ */
6
+
7
+ // Controllers
8
+ export { Controller, Button, DEFAULT_KEY_MAP } from './Controller';
9
+
10
+ // Input management
11
+ export { InputManager, type InputResult } from './InputManager';
12
+ export {
13
+ InputMapper,
14
+ type ButtonChangeCallback,
15
+ type AnalogChangeCallback,
16
+ ANALOG_INDEX,
17
+ ANALOG_AXIS,
18
+ } from './InputMapper';
19
+
20
+ // Gamepad support
21
+ export {
22
+ GamepadManager,
23
+ type GamepadButtonCallback,
24
+ type GamepadAnalogCallback,
25
+ } from './GamepadManager';
26
+ export {
27
+ type AnalogState,
28
+ type GamepadProfile,
29
+ gamepadProfiles,
30
+ findProfile,
31
+ isGamepadDevice,
32
+ } from './gamepadProfiles';
33
+
34
+ // Utilities
35
+ export { createOppositeDirections } from './inputUtils';
36
+
37
+ // Re-export constants
38
+ export * from './consts';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared input utilities for keyboard and gamepad handling.
3
+ */
4
+
5
+ /**
6
+ * Create a Map of opposite D-pad directions for preventing simultaneous
7
+ * Up+Down or Left+Right presses.
8
+ *
9
+ * The returned map accepts any button type T for lookups, returning
10
+ * the opposite direction if found, or undefined if not a directional button.
11
+ *
12
+ * @param Up - The up button value
13
+ * @param Down - The down button value
14
+ * @param Left - The left button value
15
+ * @param Right - The right button value
16
+ * @returns Map from each direction to its opposite
17
+ */
18
+ export const createOppositeDirections = <T extends number>(
19
+ Up: T,
20
+ Down: T,
21
+ Left: T,
22
+ Right: T
23
+ ): Map<T, T> => {
24
+ // Create a Map with explicit type to allow any T as lookup key
25
+ const map = new Map<T, T>();
26
+ map.set(Up, Down);
27
+ map.set(Down, Up);
28
+ map.set(Left, Right);
29
+ map.set(Right, Left);
30
+ return map;
31
+ };
@@ -0,0 +1,2 @@
1
+ /** Default number of input values per device (joypad + 2 analog) */
2
+ export const INPUTS_PER_DEVICE = 3;
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Frame Buffer Ring for Netplay Rollback
3
+ *
4
+ * Maintains a fixed-size ring buffer of frame states for rollback/replay.
5
+ * Each frame stores:
6
+ * - Serialized core state (savestate)
7
+ * - Local input
8
+ * - Remote input (per client)
9
+ * - CRC32 hash for desync detection
10
+ */
11
+
12
+ import { DEFAULT_FRAME_BUFFER_SIZE, MAX_INPUT_DEVICES, type FrameState } from '..';
13
+ import { crc32 } from '../crc32';
14
+
15
+ export * from './consts';
16
+ import { INPUTS_PER_DEVICE } from './consts';
17
+
18
+ /**
19
+ * Create a zero-filled number array of the given size.
20
+ */
21
+ const createZeroArray = (size: number): number[] => {
22
+ const arr: number[] = [];
23
+ for (let i = 0; i < size; i++) {
24
+ arr.push(0);
25
+ }
26
+ return arr;
27
+ };
28
+
29
+ /**
30
+ * Create an empty frame state for a given frame number.
31
+ */
32
+ const createEmptyFrame = (frameNumber: number): FrameState => ({
33
+ frameNumber,
34
+ serializedState: null,
35
+ localInput: createZeroArray(MAX_INPUT_DEVICES * INPUTS_PER_DEVICE),
36
+ remoteInput: new Map(),
37
+ remoteInputReal: new Map(),
38
+ crc: null,
39
+ });
40
+
41
+ /**
42
+ * FrameBuffer implements a ring buffer for frame history.
43
+ *
44
+ * Frame numbers can grow indefinitely, but the buffer only stores
45
+ * the most recent `capacity` frames. Old frames are overwritten.
46
+ */
47
+ export class FrameBuffer {
48
+ private readonly buffer: FrameState[];
49
+ private readonly _capacity: number;
50
+
51
+ /** Earliest frame number still in the buffer */
52
+ private _oldestFrame: number = 0;
53
+
54
+ /** Most recent frame number in the buffer */
55
+ private _newestFrame: number = -1;
56
+
57
+ constructor(capacity: number = DEFAULT_FRAME_BUFFER_SIZE) {
58
+ this._capacity = capacity;
59
+ this.buffer = [];
60
+
61
+ // Initialize all slots
62
+ for (let i = 0; i < capacity; i++) {
63
+ this.buffer.push(createEmptyFrame(i));
64
+ }
65
+ }
66
+
67
+ /** Buffer capacity (max frames stored) */
68
+ get capacity(): number {
69
+ return this._capacity;
70
+ }
71
+
72
+ /** Oldest frame number still available */
73
+ get oldestFrame(): number {
74
+ return this._oldestFrame;
75
+ }
76
+
77
+ /** Newest (current) frame number */
78
+ get newestFrame(): number {
79
+ return this._newestFrame;
80
+ }
81
+
82
+ /** Number of frames currently stored */
83
+ get size(): number {
84
+ if (this._newestFrame < 0) {
85
+ return 0;
86
+ }
87
+ return Math.min(this._newestFrame - this._oldestFrame + 1, this._capacity);
88
+ }
89
+
90
+ /**
91
+ * Get the buffer index for a frame number.
92
+ */
93
+ private indexFor(frameNumber: number): number {
94
+ // Handle negative frame numbers (shouldn't happen but be safe)
95
+ const n = frameNumber < 0 ? 0 : frameNumber;
96
+ return n % this._capacity;
97
+ }
98
+
99
+ /**
100
+ * Check if a frame number is within the buffer range.
101
+ */
102
+ hasFrame(frameNumber: number): boolean {
103
+ if (this._newestFrame < 0) {
104
+ return false;
105
+ }
106
+ return frameNumber >= this._oldestFrame && frameNumber <= this._newestFrame;
107
+ }
108
+
109
+ /**
110
+ * Get a frame by number. Returns null if not in buffer.
111
+ */
112
+ get(frameNumber: number): FrameState | null {
113
+ if (!this.hasFrame(frameNumber)) {
114
+ return null;
115
+ }
116
+ return this.buffer[this.indexFor(frameNumber)];
117
+ }
118
+
119
+ /**
120
+ * Get the current (newest) frame.
121
+ */
122
+ getCurrent(): FrameState | null {
123
+ if (this._newestFrame < 0) {
124
+ return null;
125
+ }
126
+ return this.buffer[this.indexFor(this._newestFrame)];
127
+ }
128
+
129
+ /**
130
+ * Advance to the next frame, returning the new frame state.
131
+ * This overwrites the oldest frame if the buffer is full.
132
+ */
133
+ advance(): FrameState {
134
+ this._newestFrame++;
135
+
136
+ // Update oldest frame if buffer is full (only when we actually need to overwrite)
137
+ const currentSize = this._newestFrame - this._oldestFrame + 1;
138
+ if (currentSize > this._capacity) {
139
+ this._oldestFrame = this._newestFrame - this._capacity + 1;
140
+ }
141
+
142
+ const index = this.indexFor(this._newestFrame);
143
+ const frame = this.buffer[index];
144
+
145
+ // Reset the frame for reuse
146
+ frame.frameNumber = this._newestFrame;
147
+ frame.serializedState = null;
148
+ frame.localInput.fill(0);
149
+ frame.remoteInput.clear();
150
+ frame.remoteInputReal.clear();
151
+ frame.crc = null;
152
+
153
+ return frame;
154
+ }
155
+
156
+ /**
157
+ * Initialize the buffer at a specific starting frame.
158
+ * Used when syncing with server.
159
+ */
160
+ initializeAt(frameNumber: number): FrameState {
161
+ this._oldestFrame = frameNumber;
162
+ this._newestFrame = frameNumber;
163
+
164
+ const index = this.indexFor(frameNumber);
165
+ const frame = this.buffer[index];
166
+
167
+ frame.frameNumber = frameNumber;
168
+ frame.serializedState = null;
169
+ frame.localInput.fill(0);
170
+ frame.remoteInput.clear();
171
+ frame.remoteInputReal.clear();
172
+ frame.crc = null;
173
+
174
+ return frame;
175
+ }
176
+
177
+ /**
178
+ * Set the serialized state for a frame.
179
+ */
180
+ setState(frameNumber: number, state: Buffer): boolean {
181
+ const frame = this.get(frameNumber);
182
+ if (!frame) {
183
+ return false;
184
+ }
185
+ frame.serializedState = state;
186
+ frame.crc = crc32(state);
187
+ return true;
188
+ }
189
+
190
+ /**
191
+ * Get the serialized state for a frame.
192
+ */
193
+ getState(frameNumber: number): Buffer | null {
194
+ const frame = this.get(frameNumber);
195
+ return frame?.serializedState ?? null;
196
+ }
197
+
198
+ /**
199
+ * Set local input for the current frame.
200
+ * Input is an array of values: [joypad, analogLeft, analogRight] per device.
201
+ */
202
+ setLocalInput(input: number[]): boolean {
203
+ const frame = this.getCurrent();
204
+ if (!frame) {
205
+ return false;
206
+ }
207
+ const len = Math.min(input.length, frame.localInput.length);
208
+ for (let i = 0; i < len; i++) {
209
+ frame.localInput[i] = input[i];
210
+ }
211
+ return true;
212
+ }
213
+
214
+ /**
215
+ * Set local input for a specific device on the current frame.
216
+ */
217
+ setLocalInputForDevice(
218
+ deviceIndex: number,
219
+ joypad: number,
220
+ analogLeft: number = 0,
221
+ analogRight: number = 0
222
+ ): boolean {
223
+ const frame = this.getCurrent();
224
+ if (!frame || deviceIndex >= MAX_INPUT_DEVICES) {
225
+ return false;
226
+ }
227
+ const base = deviceIndex * INPUTS_PER_DEVICE;
228
+ frame.localInput[base] = joypad;
229
+ frame.localInput[base + 1] = analogLeft;
230
+ frame.localInput[base + 2] = analogRight;
231
+ return true;
232
+ }
233
+
234
+ /**
235
+ * Set remote input for a specific client and frame.
236
+ * Returns true if this was new real input (not just an update).
237
+ */
238
+ setRemoteInput(
239
+ frameNumber: number,
240
+ clientId: number,
241
+ input: number[],
242
+ isReal: boolean = true
243
+ ): boolean {
244
+ const frame = this.get(frameNumber);
245
+ if (!frame) {
246
+ return false;
247
+ }
248
+
249
+ const wasReal = frame.remoteInputReal.get(clientId) ?? false;
250
+ frame.remoteInput.set(clientId, [...input]);
251
+ frame.remoteInputReal.set(clientId, isReal);
252
+
253
+ // Return true if this is new real input
254
+ return isReal && !wasReal;
255
+ }
256
+
257
+ /**
258
+ * Get remote input for a client at a specific frame.
259
+ * Returns null if not available.
260
+ */
261
+ getRemoteInput(frameNumber: number, clientId: number): number[] | null {
262
+ const frame = this.get(frameNumber);
263
+ return frame?.remoteInput.get(clientId) ?? null;
264
+ }
265
+
266
+ /**
267
+ * Check if remote input for a client at a frame is real (not simulated).
268
+ */
269
+ isRemoteInputReal(frameNumber: number, clientId: number): boolean {
270
+ const frame = this.get(frameNumber);
271
+ return frame?.remoteInputReal.get(clientId) ?? false;
272
+ }
273
+
274
+ /**
275
+ * Get the CRC32 for a frame's state.
276
+ */
277
+ getCrc(frameNumber: number): number | null {
278
+ const frame = this.get(frameNumber);
279
+ return frame?.crc ?? null;
280
+ }
281
+
282
+ /**
283
+ * Find the earliest frame that has real remote input from all specified clients.
284
+ * Used to determine the "other" sync point.
285
+ */
286
+ findSyncFrame(clientIds: number[]): number | null {
287
+ if (clientIds.length === 0 || this._newestFrame < 0) {
288
+ return this._newestFrame >= 0 ? this._newestFrame : null;
289
+ }
290
+
291
+ // Search backwards from newest to find last fully synced frame
292
+ for (let f = this._newestFrame; f >= this._oldestFrame; f--) {
293
+ const frame = this.get(f);
294
+ if (!frame) {
295
+ continue;
296
+ }
297
+
298
+ let allReal = true;
299
+ for (const clientId of clientIds) {
300
+ if (!frame.remoteInputReal.get(clientId)) {
301
+ allReal = false;
302
+ break;
303
+ }
304
+ }
305
+
306
+ if (allReal) {
307
+ return f;
308
+ }
309
+ }
310
+
311
+ return null;
312
+ }
313
+
314
+ /**
315
+ * Find the first frame where we have simulated (not real) input for any client.
316
+ * This is the "unread" frame - first frame with incomplete data.
317
+ */
318
+ findUnreadFrame(clientIds: number[]): number | null {
319
+ if (clientIds.length === 0 || this._newestFrame < 0) {
320
+ return null;
321
+ }
322
+
323
+ // Search forward from oldest to find first frame with simulated input
324
+ for (let f = this._oldestFrame; f <= this._newestFrame; f++) {
325
+ const frame = this.get(f);
326
+ if (!frame) {
327
+ continue;
328
+ }
329
+
330
+ for (const clientId of clientIds) {
331
+ if (!frame.remoteInputReal.get(clientId)) {
332
+ return f;
333
+ }
334
+ }
335
+ }
336
+
337
+ // All frames have real input
338
+ return null;
339
+ }
340
+
341
+ /**
342
+ * Clear all frames and reset the buffer.
343
+ */
344
+ clear(): void {
345
+ this._oldestFrame = 0;
346
+ this._newestFrame = -1;
347
+ for (let i = 0; i < this._capacity; i++) {
348
+ const frame = this.buffer[i];
349
+ frame.frameNumber = i;
350
+ frame.serializedState = null;
351
+ frame.localInput.fill(0);
352
+ frame.remoteInput.clear();
353
+ frame.remoteInputReal.clear();
354
+ frame.crc = null;
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Create a new frame buffer with the specified capacity.
361
+ */
362
+ export const createFrameBuffer = (capacity: number = DEFAULT_FRAME_BUFFER_SIZE): FrameBuffer => {
363
+ return new FrameBuffer(capacity);
364
+ };