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,606 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
NetplayCmd,
|
|
4
|
+
CONNECTION_MAGIC,
|
|
5
|
+
EXTENDED_HEADER_SIZE,
|
|
6
|
+
MAX_NICK_LEN,
|
|
7
|
+
CORE_NAME_LEN,
|
|
8
|
+
MAX_INPUT_DEVICES,
|
|
9
|
+
} from '..';
|
|
10
|
+
import {
|
|
11
|
+
// Protocol functions
|
|
12
|
+
createConnectionHeader,
|
|
13
|
+
validateConnectionHeader,
|
|
14
|
+
parseConnectionHeader,
|
|
15
|
+
hasValidConnectionMagic,
|
|
16
|
+
hashPassword,
|
|
17
|
+
encodeCommand,
|
|
18
|
+
decodeCommand,
|
|
19
|
+
parseCommand,
|
|
20
|
+
|
|
21
|
+
// Command builders
|
|
22
|
+
buildInputCommand,
|
|
23
|
+
buildNoInputCommand,
|
|
24
|
+
buildNickCommand,
|
|
25
|
+
buildPasswordCommand,
|
|
26
|
+
buildInfoCommand,
|
|
27
|
+
buildSyncCommand,
|
|
28
|
+
buildModeCommand,
|
|
29
|
+
buildModeRefusedCommand,
|
|
30
|
+
buildCrcCommand,
|
|
31
|
+
buildLoadSavestateCommand,
|
|
32
|
+
buildPauseCommand,
|
|
33
|
+
buildResumeCommand,
|
|
34
|
+
buildStallCommand,
|
|
35
|
+
buildResetCommand,
|
|
36
|
+
buildPlayerChatCommand,
|
|
37
|
+
buildPingRequestCommand,
|
|
38
|
+
buildPingResponseCommand,
|
|
39
|
+
buildAckCommand,
|
|
40
|
+
buildNakCommand,
|
|
41
|
+
buildDisconnectCommand,
|
|
42
|
+
buildSpectateCommand,
|
|
43
|
+
buildPlayCommand,
|
|
44
|
+
buildRequestSavestateCommand,
|
|
45
|
+
|
|
46
|
+
// Command parsers
|
|
47
|
+
parseInputCommand,
|
|
48
|
+
parseNoInputCommand,
|
|
49
|
+
parseNickCommand,
|
|
50
|
+
parsePasswordCommand,
|
|
51
|
+
parseInfoCommand,
|
|
52
|
+
parseSyncCommand,
|
|
53
|
+
parseModeCommand,
|
|
54
|
+
parseModeRefusedCommand,
|
|
55
|
+
parseCrcCommand,
|
|
56
|
+
parseLoadSavestateCommand,
|
|
57
|
+
parsePauseCommand,
|
|
58
|
+
parseStallCommand,
|
|
59
|
+
parseResetCommand,
|
|
60
|
+
parsePlayerChatCommand,
|
|
61
|
+
} from '.';
|
|
62
|
+
|
|
63
|
+
describe('Netplay Protocol', () => {
|
|
64
|
+
describe('Connection Header', () => {
|
|
65
|
+
it('should create valid 24-byte connection header', () => {
|
|
66
|
+
const header = createConnectionHeader();
|
|
67
|
+
// Extended header is 24 bytes to match RetroArch format
|
|
68
|
+
expect(header.length).toBe(24);
|
|
69
|
+
expect(header.readUInt32BE(0)).toBe(CONNECTION_MAGIC);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should create header with options (nickname sent via NICK command)', () => {
|
|
73
|
+
const header = createConnectionHeader({ isServer: false });
|
|
74
|
+
// Header is still 24 bytes
|
|
75
|
+
expect(header.length).toBe(24);
|
|
76
|
+
expect(header.readUInt32BE(0)).toBe(CONNECTION_MAGIC);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should validate correct header magic', () => {
|
|
80
|
+
const header = createConnectionHeader();
|
|
81
|
+
expect(hasValidConnectionMagic(header)).toBe(true);
|
|
82
|
+
// Legacy validation also works
|
|
83
|
+
expect(validateConnectionHeader(header)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should parse connection header', () => {
|
|
87
|
+
const header = createConnectionHeader();
|
|
88
|
+
const result = parseConnectionHeader(header);
|
|
89
|
+
// Parses the full 24-byte extended header
|
|
90
|
+
expect(result).not.toBeNull();
|
|
91
|
+
expect(result?.header.magic).toBe(CONNECTION_MAGIC);
|
|
92
|
+
expect(result?.bytesConsumed).toBe(EXTENDED_HEADER_SIZE);
|
|
93
|
+
expect(result?.header.nickname).toBe(''); // No embedded nickname
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should reject invalid header magic', () => {
|
|
97
|
+
const invalid = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
|
98
|
+
expect(hasValidConnectionMagic(invalid)).toBe(false);
|
|
99
|
+
expect(validateConnectionHeader(invalid)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should reject too-short header', () => {
|
|
103
|
+
const short = Buffer.from([0x52, 0x41]);
|
|
104
|
+
expect(hasValidConnectionMagic(short)).toBe(false);
|
|
105
|
+
expect(parseConnectionHeader(short)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return null for incomplete header', () => {
|
|
109
|
+
// Only magic, no other fields
|
|
110
|
+
const partial = Buffer.alloc(4);
|
|
111
|
+
partial.writeUInt32BE(CONNECTION_MAGIC, 0);
|
|
112
|
+
expect(hasValidConnectionMagic(partial)).toBe(true);
|
|
113
|
+
expect(parseConnectionHeader(partial)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Password Hashing', () => {
|
|
118
|
+
it('should hash password with SHA-256', () => {
|
|
119
|
+
const hash = hashPassword('test123');
|
|
120
|
+
expect(hash).toBe('ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae');
|
|
121
|
+
expect(hash.length).toBe(64);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should produce different hashes for different passwords', () => {
|
|
125
|
+
const hash1 = hashPassword('password1');
|
|
126
|
+
const hash2 = hashPassword('password2');
|
|
127
|
+
expect(hash1).not.toBe(hash2);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('Command Encoding/Decoding', () => {
|
|
132
|
+
it('should encode command with empty payload', () => {
|
|
133
|
+
const encoded = encodeCommand(NetplayCmd.ACK);
|
|
134
|
+
expect(encoded.length).toBe(8); // 4 bytes cmd + 4 bytes size
|
|
135
|
+
expect(encoded.readUInt32BE(0)).toBe(NetplayCmd.ACK);
|
|
136
|
+
expect(encoded.readUInt32BE(4)).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should encode command with payload', () => {
|
|
140
|
+
const payload = Buffer.from([1, 2, 3, 4]);
|
|
141
|
+
const encoded = encodeCommand(NetplayCmd.INPUT, payload);
|
|
142
|
+
expect(encoded.length).toBe(12); // 8 header + 4 payload
|
|
143
|
+
expect(encoded.readUInt32BE(0)).toBe(NetplayCmd.INPUT);
|
|
144
|
+
expect(encoded.readUInt32BE(4)).toBe(4);
|
|
145
|
+
expect(encoded.subarray(8)).toEqual(payload);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should decode complete command', () => {
|
|
149
|
+
const payload = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
|
|
150
|
+
const encoded = encodeCommand(NetplayCmd.CRC, payload);
|
|
151
|
+
|
|
152
|
+
const result = decodeCommand(encoded);
|
|
153
|
+
expect(result).not.toBeNull();
|
|
154
|
+
expect(result!.command.cmd).toBe(NetplayCmd.CRC);
|
|
155
|
+
expect(result!.command.payload).toEqual(payload);
|
|
156
|
+
expect(result!.bytesConsumed).toBe(12);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return null for incomplete command', () => {
|
|
160
|
+
const partial = Buffer.from([0x00, 0x00, 0x00, 0x03]); // Only cmd, no size
|
|
161
|
+
expect(decodeCommand(partial)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should return null when payload is incomplete', () => {
|
|
165
|
+
const buffer = Buffer.alloc(10);
|
|
166
|
+
buffer.writeUInt32BE(NetplayCmd.INPUT, 0);
|
|
167
|
+
buffer.writeUInt32BE(100, 4); // Claims 100 bytes but only 2 available
|
|
168
|
+
expect(decodeCommand(buffer)).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle multiple commands in buffer', () => {
|
|
172
|
+
const cmd1 = encodeCommand(NetplayCmd.ACK);
|
|
173
|
+
const cmd2 = encodeCommand(NetplayCmd.NAK);
|
|
174
|
+
const combined = Buffer.concat([cmd1, cmd2]);
|
|
175
|
+
|
|
176
|
+
const result1 = decodeCommand(combined);
|
|
177
|
+
expect(result1).not.toBeNull();
|
|
178
|
+
expect(result1!.command.cmd).toBe(NetplayCmd.ACK);
|
|
179
|
+
|
|
180
|
+
const remaining = combined.subarray(result1!.bytesConsumed);
|
|
181
|
+
const result2 = decodeCommand(remaining);
|
|
182
|
+
expect(result2).not.toBeNull();
|
|
183
|
+
expect(result2!.command.cmd).toBe(NetplayCmd.NAK);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('INPUT Command', () => {
|
|
188
|
+
it('should build INPUT command without analog', () => {
|
|
189
|
+
const encoded = buildInputCommand(1000, 1, false, 0x00ff);
|
|
190
|
+
const result = decodeCommand(encoded)!;
|
|
191
|
+
const parsed = parseInputCommand(result.command.payload);
|
|
192
|
+
|
|
193
|
+
expect(parsed.cmd).toBe(NetplayCmd.INPUT);
|
|
194
|
+
expect(parsed.frameNumber).toBe(1000);
|
|
195
|
+
expect(parsed.clientId).toBe(1);
|
|
196
|
+
expect(parsed.joypadState).toBe(0x00ff);
|
|
197
|
+
expect(parsed.analogLeft).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should build INPUT command with isServer parameter (ignored per RetroArch spec)', () => {
|
|
201
|
+
// Per RetroArch reference (send_input_frame), the isServer flag is NOT sent in the wire format
|
|
202
|
+
// The receiver determines server origin from the connection, not from the INPUT packet
|
|
203
|
+
// The isServer parameter is kept for API compatibility but ignored
|
|
204
|
+
const encoded = buildInputCommand(500, 0, true, 0x1234);
|
|
205
|
+
const result = decodeCommand(encoded)!;
|
|
206
|
+
const parsed = parseInputCommand(result.command.payload);
|
|
207
|
+
|
|
208
|
+
expect(parsed.clientId).toBe(0);
|
|
209
|
+
expect(parsed.joypadState).toBe(0x1234);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should build INPUT command with deviceBitmap parameter (ignored per RetroArch spec)', () => {
|
|
213
|
+
// Per RetroArch reference (send_input_frame), the device bitmap is NOT sent in the wire format
|
|
214
|
+
// The receiver looks up client_devices[client_num] from SYNC state to know which devices
|
|
215
|
+
// The deviceBitmap parameter is kept for API compatibility but ignored
|
|
216
|
+
const encoded = buildInputCommand(100, 2, false, 0xabcd, 0x6);
|
|
217
|
+
const result = decodeCommand(encoded)!;
|
|
218
|
+
const parsed = parseInputCommand(result.command.payload);
|
|
219
|
+
|
|
220
|
+
expect(parsed.joypadState).toBe(0xabcd);
|
|
221
|
+
expect(parsed.clientId).toBe(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should round-trip through parseCommand', () => {
|
|
225
|
+
const encoded = buildInputCommand(999, 5, true, 0x5555);
|
|
226
|
+
const result = decodeCommand(encoded)!;
|
|
227
|
+
const parsed = parseCommand(result.command);
|
|
228
|
+
|
|
229
|
+
expect(parsed.cmd).toBe(NetplayCmd.INPUT);
|
|
230
|
+
if ('frameNumber' in parsed) {
|
|
231
|
+
expect(parsed.frameNumber).toBe(999);
|
|
232
|
+
}
|
|
233
|
+
if ('clientId' in parsed) {
|
|
234
|
+
expect(parsed.clientId).toBe(5);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('NOINPUT Command', () => {
|
|
240
|
+
it('should build and parse NOINPUT', () => {
|
|
241
|
+
const encoded = buildNoInputCommand(12345);
|
|
242
|
+
const result = decodeCommand(encoded)!;
|
|
243
|
+
const parsed = parseNoInputCommand(result.command.payload);
|
|
244
|
+
|
|
245
|
+
expect(parsed.cmd).toBe(NetplayCmd.NOINPUT);
|
|
246
|
+
expect(parsed.frameNumber).toBe(12345);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('NICK Command', () => {
|
|
251
|
+
it('should build and parse NICK', () => {
|
|
252
|
+
const encoded = buildNickCommand('Player1');
|
|
253
|
+
const result = decodeCommand(encoded)!;
|
|
254
|
+
const parsed = parseNickCommand(result.command.payload);
|
|
255
|
+
|
|
256
|
+
expect(parsed.cmd).toBe(NetplayCmd.NICK);
|
|
257
|
+
expect(parsed.nickname).toBe('Player1');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should truncate long nicknames', () => {
|
|
261
|
+
const longNick = 'A'.repeat(100);
|
|
262
|
+
const encoded = buildNickCommand(longNick);
|
|
263
|
+
const result = decodeCommand(encoded)!;
|
|
264
|
+
const parsed = parseNickCommand(result.command.payload);
|
|
265
|
+
|
|
266
|
+
expect(parsed.nickname.length).toBeLessThanOrEqual(MAX_NICK_LEN - 1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle empty nickname', () => {
|
|
270
|
+
const encoded = buildNickCommand('');
|
|
271
|
+
const result = decodeCommand(encoded)!;
|
|
272
|
+
const parsed = parseNickCommand(result.command.payload);
|
|
273
|
+
|
|
274
|
+
expect(parsed.nickname).toBe('');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('PASSWORD Command', () => {
|
|
279
|
+
it('should build and parse PASSWORD', () => {
|
|
280
|
+
const hash = hashPassword('secret');
|
|
281
|
+
const encoded = buildPasswordCommand(hash);
|
|
282
|
+
const result = decodeCommand(encoded)!;
|
|
283
|
+
const parsed = parsePasswordCommand(result.command.payload);
|
|
284
|
+
|
|
285
|
+
expect(parsed.cmd).toBe(NetplayCmd.PASSWORD);
|
|
286
|
+
expect(parsed.passwordHash).toBe(hash);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('INFO Command', () => {
|
|
291
|
+
it('should build and parse INFO', () => {
|
|
292
|
+
const encoded = buildInfoCommand('bsnes', '115.1', 0xdeadbeef);
|
|
293
|
+
const result = decodeCommand(encoded)!;
|
|
294
|
+
const parsed = parseInfoCommand(result.command.payload);
|
|
295
|
+
|
|
296
|
+
expect(parsed.cmd).toBe(NetplayCmd.INFO);
|
|
297
|
+
expect(parsed.coreName).toBe('bsnes');
|
|
298
|
+
expect(parsed.coreVersion).toBe('115.1');
|
|
299
|
+
expect(parsed.contentCrc).toBe(0xdeadbeef);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should truncate long core names', () => {
|
|
303
|
+
const longName = 'X'.repeat(100);
|
|
304
|
+
const encoded = buildInfoCommand(longName, '1.0', 0);
|
|
305
|
+
const result = decodeCommand(encoded)!;
|
|
306
|
+
const parsed = parseInfoCommand(result.command.payload);
|
|
307
|
+
|
|
308
|
+
expect(parsed.coreName.length).toBeLessThanOrEqual(CORE_NAME_LEN - 1);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('SYNC Command', () => {
|
|
313
|
+
it('should build and parse SYNC', () => {
|
|
314
|
+
const devices = [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
315
|
+
const shareModes = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
316
|
+
const deviceClients = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
317
|
+
const sram = Buffer.from([1, 2, 3, 4, 5]);
|
|
318
|
+
const encoded = buildSyncCommand(1000, false, 2, devices, shareModes, deviceClients, 'Host', sram);
|
|
319
|
+
const result = decodeCommand(encoded)!;
|
|
320
|
+
const parsed = parseSyncCommand(result.command.payload);
|
|
321
|
+
|
|
322
|
+
expect(parsed.cmd).toBe(NetplayCmd.SYNC);
|
|
323
|
+
expect(parsed.frameNumber).toBe(1000);
|
|
324
|
+
expect(parsed.paused).toBe(false);
|
|
325
|
+
expect(parsed.clientNumber).toBe(2);
|
|
326
|
+
expect(parsed.devices.slice(0, 2)).toEqual([1, 1]);
|
|
327
|
+
expect(parsed.clientNick).toBe('Host');
|
|
328
|
+
expect(parsed.sram).toEqual(sram);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should handle paused state', () => {
|
|
332
|
+
const devices = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
333
|
+
const shareModes = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
334
|
+
const deviceClients = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
335
|
+
const encoded = buildSyncCommand(500, true, 1, devices, shareModes, deviceClients, 'Pauser', Buffer.alloc(0));
|
|
336
|
+
const result = decodeCommand(encoded)!;
|
|
337
|
+
const parsed = parseSyncCommand(result.command.payload);
|
|
338
|
+
|
|
339
|
+
expect(parsed.paused).toBe(true);
|
|
340
|
+
expect(parsed.clientNumber).toBe(1);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('MODE Command', () => {
|
|
345
|
+
it('should build and parse MODE for playing', () => {
|
|
346
|
+
const shareModes = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
347
|
+
const encoded = buildModeCommand(5000, true, true, false, 1, 0b11, shareModes, 'Player1');
|
|
348
|
+
const result = decodeCommand(encoded)!;
|
|
349
|
+
const parsed = parseModeCommand(result.command.payload);
|
|
350
|
+
|
|
351
|
+
expect(parsed.cmd).toBe(NetplayCmd.MODE);
|
|
352
|
+
expect(parsed.frameNumber).toBe(5000);
|
|
353
|
+
expect(parsed.you).toBe(true);
|
|
354
|
+
expect(parsed.playing).toBe(true);
|
|
355
|
+
expect(parsed.slave).toBe(false);
|
|
356
|
+
expect(parsed.clientNumber).toBe(1);
|
|
357
|
+
expect(parsed.deviceBitmap).toBe(0b11);
|
|
358
|
+
expect(parsed.nick).toBe('Player1');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should build and parse MODE for spectating', () => {
|
|
362
|
+
const shareModes = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
363
|
+
const encoded = buildModeCommand(100, true, false, false, 0, 0, shareModes, 'Spectator');
|
|
364
|
+
const result = decodeCommand(encoded)!;
|
|
365
|
+
const parsed = parseModeCommand(result.command.payload);
|
|
366
|
+
|
|
367
|
+
expect(parsed.playing).toBe(false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should handle slave mode', () => {
|
|
371
|
+
const shareModes = new Array(MAX_INPUT_DEVICES).fill(0);
|
|
372
|
+
const encoded = buildModeCommand(200, false, true, true, 2, 0b01, shareModes, 'Slave');
|
|
373
|
+
const result = decodeCommand(encoded)!;
|
|
374
|
+
const parsed = parseModeCommand(result.command.payload);
|
|
375
|
+
|
|
376
|
+
expect(parsed.slave).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('MODE_REFUSED Command', () => {
|
|
381
|
+
it('should build and parse MODE_REFUSED', () => {
|
|
382
|
+
const encoded = buildModeRefusedCommand(1); // NO_SLOTS
|
|
383
|
+
const result = decodeCommand(encoded)!;
|
|
384
|
+
const parsed = parseModeRefusedCommand(result.command.payload);
|
|
385
|
+
|
|
386
|
+
expect(parsed.cmd).toBe(NetplayCmd.MODE_REFUSED);
|
|
387
|
+
expect(parsed.reason).toBe(1);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('CRC Command', () => {
|
|
392
|
+
it('should build and parse CRC', () => {
|
|
393
|
+
const encoded = buildCrcCommand(10000, 0xcafebabe);
|
|
394
|
+
const result = decodeCommand(encoded)!;
|
|
395
|
+
const parsed = parseCrcCommand(result.command.payload);
|
|
396
|
+
|
|
397
|
+
expect(parsed.cmd).toBe(NetplayCmd.CRC);
|
|
398
|
+
expect(parsed.frameNumber).toBe(10000);
|
|
399
|
+
expect(parsed.crc).toBe(0xcafebabe);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('LOAD_SAVESTATE Command', () => {
|
|
404
|
+
/**
|
|
405
|
+
* Calculate the expected NETPLAY-wrapped size for a raw state.
|
|
406
|
+
* Format: NETPLAY header (8) + MEM block (8 + data + padding to 16) + ACHV block (16) + END block (8)
|
|
407
|
+
*/
|
|
408
|
+
const calculateNetplayWrappedSize = (rawSize: number): number => {
|
|
409
|
+
const HEADER_SIZE = 8;
|
|
410
|
+
const BLOCK_HEADER_SIZE = 8;
|
|
411
|
+
const ALIGNMENT = 16;
|
|
412
|
+
const memDataEnd = HEADER_SIZE + BLOCK_HEADER_SIZE + rawSize;
|
|
413
|
+
const memPaddedEnd = Math.ceil(memDataEnd / ALIGNMENT) * ALIGNMENT;
|
|
414
|
+
const achvBlockSize = BLOCK_HEADER_SIZE + 8; // 16
|
|
415
|
+
const endBlockSize = BLOCK_HEADER_SIZE; // 8
|
|
416
|
+
return memPaddedEnd + achvBlockSize + endBlockSize;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
it('should build LOAD_SAVESTATE with zlib compression and parse back to original state', () => {
|
|
420
|
+
const rawState = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]);
|
|
421
|
+
const encoded = buildLoadSavestateCommand(2000, rawState, 7);
|
|
422
|
+
const result = decodeCommand(encoded)!;
|
|
423
|
+
const parsed = parseLoadSavestateCommand(result.command.payload);
|
|
424
|
+
|
|
425
|
+
expect(parsed.cmd).toBe(NetplayCmd.LOAD_SAVESTATE);
|
|
426
|
+
expect(parsed.frameNumber).toBe(2000);
|
|
427
|
+
|
|
428
|
+
// State should be decompressed and unwrapped back to original
|
|
429
|
+
expect(parsed.state).toEqual(rawState);
|
|
430
|
+
// uncompressed_size should be the NETPLAY-wrapped size (not raw state length)
|
|
431
|
+
expect(parsed.uncompressedSize).toBe(calculateNetplayWrappedSize(rawState.length));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should compress and decompress state data regardless of protocol version', () => {
|
|
435
|
+
const rawState = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]);
|
|
436
|
+
const encoded = buildLoadSavestateCommand(2000, rawState, 4);
|
|
437
|
+
const result = decodeCommand(encoded)!;
|
|
438
|
+
const parsed = parseLoadSavestateCommand(result.command.payload);
|
|
439
|
+
|
|
440
|
+
expect(parsed.cmd).toBe(NetplayCmd.LOAD_SAVESTATE);
|
|
441
|
+
expect(parsed.frameNumber).toBe(2000);
|
|
442
|
+
// State should be decompressed and unwrapped back to original
|
|
443
|
+
expect(parsed.state).toEqual(rawState);
|
|
444
|
+
// uncompressed_size should be the NETPLAY-wrapped size
|
|
445
|
+
expect(parsed.uncompressedSize).toBe(calculateNetplayWrappedSize(rawState.length));
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should round-trip compressible data correctly', () => {
|
|
449
|
+
// Create a larger, more compressible buffer (repeated data)
|
|
450
|
+
const rawState = Buffer.alloc(1000, 0xaa);
|
|
451
|
+
const encoded = buildLoadSavestateCommand(2000, rawState);
|
|
452
|
+
const result = decodeCommand(encoded)!;
|
|
453
|
+
const parsed = parseLoadSavestateCommand(result.command.payload);
|
|
454
|
+
|
|
455
|
+
// State should be decompressed and unwrapped back to original
|
|
456
|
+
expect(parsed.state).toEqual(rawState);
|
|
457
|
+
expect(parsed.uncompressedSize).toBe(calculateNetplayWrappedSize(rawState.length));
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe('PAUSE/RESUME Commands', () => {
|
|
462
|
+
it('should build and parse PAUSE', () => {
|
|
463
|
+
const encoded = buildPauseCommand('Pauser');
|
|
464
|
+
const result = decodeCommand(encoded)!;
|
|
465
|
+
const parsed = parsePauseCommand(result.command.payload);
|
|
466
|
+
|
|
467
|
+
expect(parsed.cmd).toBe(NetplayCmd.PAUSE);
|
|
468
|
+
expect(parsed.nickname).toBe('Pauser');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should build RESUME', () => {
|
|
472
|
+
const encoded = buildResumeCommand();
|
|
473
|
+
const result = decodeCommand(encoded)!;
|
|
474
|
+
expect(result.command.cmd).toBe(NetplayCmd.RESUME);
|
|
475
|
+
expect(result.command.payload.length).toBe(0);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe('STALL Command', () => {
|
|
480
|
+
it('should build and parse STALL', () => {
|
|
481
|
+
const encoded = buildStallCommand(5);
|
|
482
|
+
const result = decodeCommand(encoded)!;
|
|
483
|
+
const parsed = parseStallCommand(result.command.payload);
|
|
484
|
+
|
|
485
|
+
expect(parsed.cmd).toBe(NetplayCmd.STALL);
|
|
486
|
+
expect(parsed.frames).toBe(5);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('RESET Command', () => {
|
|
491
|
+
it('should build and parse RESET', () => {
|
|
492
|
+
const encoded = buildResetCommand(7500);
|
|
493
|
+
const result = decodeCommand(encoded)!;
|
|
494
|
+
const parsed = parseResetCommand(result.command.payload);
|
|
495
|
+
|
|
496
|
+
expect(parsed.cmd).toBe(NetplayCmd.RESET);
|
|
497
|
+
expect(parsed.frameNumber).toBe(7500);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe('PLAYER_CHAT Command', () => {
|
|
502
|
+
it('should build and parse PLAYER_CHAT', () => {
|
|
503
|
+
const encoded = buildPlayerChatCommand('Alice', 'Hello, world!');
|
|
504
|
+
const result = decodeCommand(encoded)!;
|
|
505
|
+
const parsed = parsePlayerChatCommand(result.command.payload);
|
|
506
|
+
|
|
507
|
+
expect(parsed.cmd).toBe(NetplayCmd.PLAYER_CHAT);
|
|
508
|
+
expect(parsed.nickname).toBe('Alice');
|
|
509
|
+
expect(parsed.message).toBe('Hello, world!');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('should handle unicode messages', () => {
|
|
513
|
+
const encoded = buildPlayerChatCommand('Bob', 'こんにちは! 🎮');
|
|
514
|
+
const result = decodeCommand(encoded)!;
|
|
515
|
+
const parsed = parsePlayerChatCommand(result.command.payload);
|
|
516
|
+
|
|
517
|
+
expect(parsed.message).toBe('こんにちは! 🎮');
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe('PING Commands', () => {
|
|
522
|
+
it('should build and parse PING_REQUEST', () => {
|
|
523
|
+
const encoded = buildPingRequestCommand();
|
|
524
|
+
const result = decodeCommand(encoded)!;
|
|
525
|
+
|
|
526
|
+
expect(result.command.cmd).toBe(NetplayCmd.PING_REQUEST);
|
|
527
|
+
// Per RetroArch spec, PING commands have no payload
|
|
528
|
+
expect(result.command.payload.length).toBe(0);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should build and parse PING_RESPONSE', () => {
|
|
532
|
+
const encoded = buildPingResponseCommand();
|
|
533
|
+
const result = decodeCommand(encoded)!;
|
|
534
|
+
|
|
535
|
+
expect(result.command.cmd).toBe(NetplayCmd.PING_RESPONSE);
|
|
536
|
+
// Per RetroArch spec, PING commands have no payload
|
|
537
|
+
expect(result.command.payload.length).toBe(0);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('Simple Commands', () => {
|
|
542
|
+
it('should build ACK', () => {
|
|
543
|
+
const encoded = buildAckCommand();
|
|
544
|
+
const result = decodeCommand(encoded)!;
|
|
545
|
+
expect(result.command.cmd).toBe(NetplayCmd.ACK);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should build NAK', () => {
|
|
549
|
+
const encoded = buildNakCommand();
|
|
550
|
+
const result = decodeCommand(encoded)!;
|
|
551
|
+
expect(result.command.cmd).toBe(NetplayCmd.NAK);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should build DISCONNECT', () => {
|
|
555
|
+
const encoded = buildDisconnectCommand();
|
|
556
|
+
const result = decodeCommand(encoded)!;
|
|
557
|
+
expect(result.command.cmd).toBe(NetplayCmd.DISCONNECT);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should build SPECTATE', () => {
|
|
561
|
+
const encoded = buildSpectateCommand();
|
|
562
|
+
const result = decodeCommand(encoded)!;
|
|
563
|
+
expect(result.command.cmd).toBe(NetplayCmd.SPECTATE);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should build PLAY', () => {
|
|
567
|
+
const encoded = buildPlayCommand();
|
|
568
|
+
const result = decodeCommand(encoded)!;
|
|
569
|
+
expect(result.command.cmd).toBe(NetplayCmd.PLAY);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('should build REQUEST_SAVESTATE', () => {
|
|
573
|
+
const encoded = buildRequestSavestateCommand();
|
|
574
|
+
const result = decodeCommand(encoded)!;
|
|
575
|
+
expect(result.command.cmd).toBe(NetplayCmd.REQUEST_SAVESTATE);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
describe('parseCommand dispatcher', () => {
|
|
580
|
+
it('should dispatch to correct parser for INPUT', () => {
|
|
581
|
+
const encoded = buildInputCommand(100, 1, false, 0xff);
|
|
582
|
+
const result = decodeCommand(encoded)!;
|
|
583
|
+
const parsed = parseCommand(result.command);
|
|
584
|
+
|
|
585
|
+
expect(parsed.cmd).toBe(NetplayCmd.INPUT);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should dispatch to correct parser for INFO', () => {
|
|
589
|
+
const encoded = buildInfoCommand('test', '1.0', 123);
|
|
590
|
+
const result = decodeCommand(encoded)!;
|
|
591
|
+
const parsed = parseCommand(result.command);
|
|
592
|
+
|
|
593
|
+
expect(parsed.cmd).toBe(NetplayCmd.INFO);
|
|
594
|
+
if ('coreName' in parsed) {
|
|
595
|
+
expect(parsed.coreName).toBe('test');
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should return UnknownCommand for unknown command', () => {
|
|
600
|
+
const raw = { cmd: 0xffff as NetplayCmd, payload: Buffer.alloc(0) };
|
|
601
|
+
const parsed = parseCommand(raw);
|
|
602
|
+
// Unknown commands are returned as-is for forward compatibility
|
|
603
|
+
expect(parsed.cmd).toBe(0xffff);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netplay protocol error types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createTypedError } from '../../utils/typedError';
|
|
6
|
+
|
|
7
|
+
export type ProtocolErrorCode =
|
|
8
|
+
| 'INVALID_INPUT_PAYLOAD'
|
|
9
|
+
| 'INVALID_NOINPUT_PAYLOAD'
|
|
10
|
+
| 'INVALID_INFO_PAYLOAD'
|
|
11
|
+
| 'INVALID_SYNC_PAYLOAD'
|
|
12
|
+
| 'INVALID_MODE_PAYLOAD'
|
|
13
|
+
| 'INVALID_CRC_PAYLOAD'
|
|
14
|
+
| 'INVALID_SAVESTATE_PAYLOAD'
|
|
15
|
+
| 'DECOMPRESS_FAILED';
|
|
16
|
+
|
|
17
|
+
const { TypedError, isTypedError } = createTypedError<ProtocolErrorCode>('ProtocolError');
|
|
18
|
+
export const ProtocolError = TypedError;
|
|
19
|
+
export type ProtocolError = InstanceType<typeof TypedError>;
|
|
20
|
+
export const isProtocolError = isTypedError;
|