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,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,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
|
+
};
|