• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

project-slippi / slippi-js / 20253990934

16 Dec 2025 02:04AM UTC coverage: 80.856% (+0.04%) from 80.813%
20253990934

Pull #158

github

web-flow
Merge c7b30343f into 414f15082
Pull Request #158: refactor: prefer undefined over null

696 of 929 branches covered (74.92%)

Branch coverage included in aggregate %.

62 of 81 new or added lines in 20 files covered. (76.54%)

14 existing lines in 2 files now uncovered.

1855 of 2226 relevant lines covered (83.33%)

125846.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.43
/src/common/utils/slpReader.ts
1
import { decode as decodeUBJSON } from "@shelacek/ubjson";
12✔
2
import { decode as decodeSJIS } from "iconv-cp932";
12✔
3
import mapValues from "lodash/mapValues";
12✔
4

5
import type {
6
  EventCallbackFunc,
7
  EventPayloadTypes,
8
  GameEndType,
9
  GameInfoType,
10
  GameStartType,
11
  GeckoCodeType,
12
  MetadataType,
13
  PlacementType,
14
  PlayerType,
15
  PostFrameUpdateType,
16
  SelfInducedSpeedsType,
17
} from "../types";
18
import { Command } from "../types";
12✔
19
import { exists } from "./exists";
12✔
20
import { toHalfwidth } from "./fullwidth";
12✔
21
import type { SlpInputRef } from "./slpInputRef";
22

23
const utf8Decoder = new TextDecoder("utf-8");
12✔
24

25
export type SlpFileType = {
26
  ref: SlpInputRef;
27
  rawDataPosition: number;
28
  rawDataLength: number;
29
  metadataPosition: number;
30
  metadataLength: number;
31
  messageSizes: {
32
    [command: number]: number;
33
  };
34
};
35

36
export function openSlpFile(ref: SlpInputRef): SlpFileType {
12✔
37
  ref.open();
122✔
38
  const rawDataPosition = getRawDataPosition(ref);
122✔
39
  const rawDataLength = getRawDataLength(ref, rawDataPosition);
122✔
40
  const metadataPosition = rawDataPosition + rawDataLength + 10; // remove metadata string
122✔
41
  const metadataLength = getMetadataLength(ref, metadataPosition);
122✔
42
  const messageSizes = getMessageSizes(ref, rawDataPosition);
122✔
43

44
  return {
122✔
45
    ref,
46
    rawDataPosition,
47
    rawDataLength,
48
    metadataPosition,
49
    metadataLength,
50
    messageSizes,
51
  };
52
}
53

54
// This function gets the position where the raw data starts
55
function getRawDataPosition(ref: SlpInputRef): number {
56
  const buffer = new Uint8Array(1);
122✔
57
  ref.read(buffer, 0, buffer.length, 0);
122✔
58

59
  if (buffer[0] === 0x36) {
122!
UNCOV
60
    return 0;
×
61
  }
62

63
  if (buffer[0] !== "{".charCodeAt(0)) {
122✔
64
    return 0; // return error?
6✔
65
  }
66

67
  return 15;
116✔
68
}
69

70
function getRawDataLength(ref: SlpInputRef, position: number): number {
71
  const fileSize = ref.size();
122✔
72
  if (position === 0) {
122✔
73
    return fileSize;
6✔
74
  }
75

76
  const buffer = new Uint8Array(4);
116✔
77
  ref.read(buffer, 0, buffer.length, position - 4);
116✔
78

79
  const rawDataLen = (buffer[0]! << 24) | (buffer[1]! << 16) | (buffer[2]! << 8) | buffer[3]!;
116✔
80
  if (rawDataLen > 0) {
116✔
81
    // If this method manages to read a number, it's probably trustworthy
82
    return rawDataLen;
113✔
83
  }
84

85
  // If the above does not return a valid data length,
86
  // return a file size based on file length. This enables
87
  // some support for severed files
88
  return fileSize - position;
3✔
89
}
90

91
function getMetadataLength(ref: SlpInputRef, position: number): number {
92
  const len = ref.size();
122✔
93
  return len - position - 1;
122✔
94
}
95

96
function getMessageSizes(
97
  ref: SlpInputRef,
98
  position: number,
99
): {
100
  [command: number]: number;
101
} {
102
  const messageSizes: {
103
    [command: number]: number;
104
  } = {};
122✔
105
  // Support old file format
106
  if (position === 0) {
122✔
107
    messageSizes[0x36] = 0x140;
6✔
108
    messageSizes[0x37] = 0x6;
6✔
109
    messageSizes[0x38] = 0x46;
6✔
110
    messageSizes[0x39] = 0x1;
6✔
111
    return messageSizes;
6✔
112
  }
113

114
  const buffer = new Uint8Array(2);
116✔
115
  ref.read(buffer, 0, buffer.length, position);
116✔
116
  if (buffer[0] !== Command.MESSAGE_SIZES) {
116!
UNCOV
117
    return {};
×
118
  }
119

120
  const payloadLength = buffer[1] as number;
116✔
121
  (messageSizes[0x35] as any) = payloadLength;
116✔
122

123
  const messageSizesBuffer = new Uint8Array(payloadLength - 1);
116✔
124
  ref.read(messageSizesBuffer, 0, messageSizesBuffer.length, position + 2);
116✔
125
  for (let i = 0; i < payloadLength - 1; i += 3) {
116✔
126
    const command = messageSizesBuffer[i] as number;
837✔
127

128
    // Get size of command
129
    (messageSizes[command] as any) = (messageSizesBuffer[i + 1]! << 8) | messageSizesBuffer[i + 2]!;
837✔
130
  }
131

132
  return messageSizes;
116✔
133
}
134

135
function getEnabledItems(view: DataView): number {
136
  const offsets = [0x1, 0x100, 0x10000, 0x1000000, 0x100000000];
81✔
137
  const enabledItems = offsets.reduce((acc, byteOffset, index) => {
81✔
138
    const byte = readUint8(view, 0x28 + index) as number;
405✔
139
    return acc + byte * byteOffset;
405✔
140
  }, 0);
141

142
  return enabledItems;
81✔
143
}
144

145
function getGameInfoBlock(view: DataView): GameInfoType {
146
  const offset = 0x5;
81✔
147

148
  return {
81✔
149
    gameBitfield1: readUint8(view, 0x0 + offset),
150
    gameBitfield2: readUint8(view, 0x1 + offset),
151
    gameBitfield3: readUint8(view, 0x2 + offset),
152
    gameBitfield4: readUint8(view, 0x3 + offset),
153
    bombRainEnabled: (readUint8(view, 0x6 + offset)! & 0xff) > 0 ? true : false,
81!
154
    selfDestructScoreValue: readInt8(view, 0xc + offset),
155
    itemSpawnBitfield1: readUint8(view, 0x23 + offset),
156
    itemSpawnBitfield2: readUint8(view, 0x24 + offset),
157
    itemSpawnBitfield3: readUint8(view, 0x25 + offset),
158
    itemSpawnBitfield4: readUint8(view, 0x26 + offset),
159
    itemSpawnBitfield5: readUint8(view, 0x27 + offset),
160
    damageRatio: readFloat(view, 0x30 + offset),
161
  };
162
}
163

164
/**
165
 * Iterates through slp events and parses payloads
166
 */
167
export function iterateEvents(
12✔
168
  slpFile: SlpFileType,
169
  callback: EventCallbackFunc,
170
  startPos: number | undefined = undefined,
71✔
171
): number {
172
  const ref = slpFile.ref;
97✔
173

174
  let readPosition = startPos != null && startPos > 0 ? startPos : slpFile.rawDataPosition;
97✔
175
  const stopReadingAt = slpFile.rawDataPosition + slpFile.rawDataLength;
97✔
176

177
  // Generate read buffers for each
178
  const commandPayloadBuffers = mapValues(slpFile.messageSizes, (size) => new Uint8Array(size + 1));
788✔
179
  let splitMessageBuffer = new Uint8Array(0);
97✔
180

181
  const commandByteBuffer = new Uint8Array(1);
97✔
182
  while (readPosition < stopReadingAt) {
97✔
183
    ref.read(commandByteBuffer, 0, 1, readPosition);
1,329,207✔
184
    let commandByte = (commandByteBuffer[0] as number) ?? 0;
1,329,207!
185
    let buffer = commandPayloadBuffers[commandByte];
1,329,207✔
186
    if (buffer == null) {
1,329,207✔
187
      // If we don't have an entry for this command, return false to indicate failed read
188
      return readPosition;
18✔
189
    }
190

191
    if (buffer.length > stopReadingAt - readPosition) {
1,329,189!
UNCOV
192
      return readPosition;
×
193
    }
194

195
    const advanceAmount = buffer.length;
1,329,189✔
196

197
    ref.read(buffer, 0, buffer.length, readPosition);
1,329,189✔
198
    if (commandByte === Command.SPLIT_MESSAGE) {
1,329,189✔
199
      // Here we have a split message, we will collect data from them until the last
200
      // message of the list is received
201
      const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
4,123✔
202
      const size = readUint16(view, 0x201) ?? 512;
4,123!
203
      const isLastMessage = readBool(view, 0x204);
4,123✔
204
      const internalCommand = readUint8(view, 0x203) ?? 0;
4,123!
205

206
      // If this is the first message, initialize the splitMessageBuffer
207
      // with the internal command byte because our parseMessage function
208
      // seems to expect a command byte at the start
209
      if (splitMessageBuffer.length === 0) {
4,123✔
210
        splitMessageBuffer = new Uint8Array(1);
46✔
211
        splitMessageBuffer[0] = internalCommand;
46✔
212
      }
213

214
      // Collect new data into splitMessageBuffer
215
      const appendBuf = buffer.subarray(0x1, 0x1 + size);
4,123✔
216
      const mergedBuf = new Uint8Array(splitMessageBuffer.length + appendBuf.length);
4,123✔
217
      mergedBuf.set(splitMessageBuffer);
4,123✔
218
      mergedBuf.set(appendBuf, splitMessageBuffer.length);
4,123✔
219
      splitMessageBuffer = mergedBuf;
4,123✔
220

221
      if (isLastMessage) {
4,123✔
222
        commandByte = splitMessageBuffer[0] ?? 0;
46!
223
        buffer = splitMessageBuffer;
46✔
224
        splitMessageBuffer = new Uint8Array(0);
46✔
225
      }
226
    }
227

228
    const parsedPayload = parseMessage(commandByte, buffer);
1,329,189✔
229
    const shouldStop = callback(commandByte, parsedPayload, buffer);
1,329,189✔
230
    if (shouldStop) {
1,329,189✔
231
      break;
27✔
232
    }
233

234
    readPosition += advanceAmount;
1,329,162✔
235
  }
236

237
  return readPosition;
79✔
238
}
239

240
export function parseMessage(command: Command, payload: Uint8Array): EventPayloadTypes | undefined {
12✔
241
  const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
1,907,049✔
242
  switch (command) {
1,907,049✔
243
    case Command.GAME_START:
244
      const getPlayerObject = (playerIndex: number): PlayerType => {
81✔
245
        // Controller Fix stuff
246
        const cfOffset = playerIndex * 0x8;
324✔
247
        const dashback = readUint32(view, 0x141 + cfOffset);
324✔
248
        const shieldDrop = readUint32(view, 0x145 + cfOffset);
324✔
249
        let controllerFix = "None";
324✔
250
        if (dashback !== shieldDrop) {
324!
UNCOV
251
          controllerFix = "Mixed";
×
252
        } else if (dashback === 1) {
324✔
253
          controllerFix = "UCF";
219✔
254
        } else if (dashback === 2) {
105✔
255
          controllerFix = "Dween";
1✔
256
        }
257

258
        // Nametag stuff
259
        const nametagLength = 0x10;
324✔
260
        const nametagOffset = playerIndex * nametagLength;
324✔
261
        const nametagStart = 0x161 + nametagOffset;
324✔
262
        const nametagBuf = payload.subarray(nametagStart, nametagStart + nametagLength);
324✔
263
        const nameTagString = decodeSJIS(nametagBuf).split("\0").shift();
324✔
264
        const nametag = nameTagString ? toHalfwidth(nameTagString) : "";
324✔
265

266
        // Display name
267
        const displayNameLength = 0x1f;
324✔
268
        const displayNameOffset = playerIndex * displayNameLength;
324✔
269
        const displayNameStart = 0x1a5 + displayNameOffset;
324✔
270
        const displayNameBuf = payload.subarray(displayNameStart, displayNameStart + displayNameLength);
324✔
271
        const displayNameString = decodeSJIS(displayNameBuf).split("\0").shift();
324✔
272
        const displayName = displayNameString ? toHalfwidth(displayNameString) : "";
324✔
273

274
        // Connect code
275
        const connectCodeLength = 0xa;
324✔
276
        const connectCodeOffset = playerIndex * connectCodeLength;
324✔
277
        const connectCodeStart = 0x221 + connectCodeOffset;
324✔
278
        const connectCodeBuf = payload.subarray(connectCodeStart, connectCodeStart + connectCodeLength);
324✔
279
        const connectCodeString = decodeSJIS(connectCodeBuf).split("\0").shift();
324✔
280
        const connectCode = connectCodeString ? toHalfwidth(connectCodeString) : "";
324✔
281

282
        const userIdLength = 0x1d;
324✔
283
        const userIdOffset = playerIndex * userIdLength;
324✔
284
        const userIdStart = 0x249 + userIdOffset;
324✔
285
        const userIdBuf = payload.subarray(userIdStart, userIdStart + userIdLength);
324✔
286
        const userIdString = utf8Decoder.decode(userIdBuf).split("\0").shift();
324✔
287
        const userId = userIdString ?? "";
324!
288

289
        const offset = playerIndex * 0x24;
324✔
290
        const playerInfo: PlayerType = {
324✔
291
          playerIndex,
292
          port: playerIndex + 1,
293
          characterId: readUint8(view, 0x65 + offset),
294
          type: readUint8(view, 0x66 + offset),
295
          startStocks: readUint8(view, 0x67 + offset),
296
          characterColor: readUint8(view, 0x68 + offset),
297
          teamShade: readUint8(view, 0x6c + offset),
298
          handicap: readUint8(view, 0x6d + offset),
299
          teamId: readUint8(view, 0x6e + offset),
300
          staminaMode: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x01)),
301
          silentCharacter: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x02)),
302
          lowGravity: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x04)),
303
          invisible: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x08)),
304
          blackStockIcon: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x10)),
305
          metal: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x20)),
306
          startOnAngelPlatform: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x40)),
307
          rumbleEnabled: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x80)),
308
          cpuLevel: readUint8(view, 0x74 + offset),
309
          offenseRatio: readFloat(view, 0x7d + offset),
310
          defenseRatio: readFloat(view, 0x81 + offset),
311
          modelScale: readFloat(view, 0x85 + offset),
312
          controllerFix,
313
          nametag,
314
          displayName,
315
          connectCode,
316
          userId,
317
        };
318
        return playerInfo;
324✔
319
      };
320

321
      const sessionIdLength = 51;
81✔
322
      const sessionIdStart = 0x2be;
81✔
323
      const sessionIdBuf = payload.subarray(sessionIdStart, sessionIdStart + sessionIdLength);
81✔
324
      const sessionIdString = utf8Decoder.decode(sessionIdBuf).split("\0").shift();
81✔
325
      const sessionId = sessionIdString ?? "";
81!
326

327
      const gameSettings: GameStartType = {
81✔
328
        slpVersion: `${readUint8(view, 0x1)}.${readUint8(view, 0x2)}.${readUint8(view, 0x3)}`,
329
        timerType: readUint8(view, 0x5, 0x03),
330
        inGameMode: readUint8(view, 0x5, 0xe0),
331
        friendlyFireEnabled: !!readUint8(view, 0x6, 0x01),
332
        isTeams: readBool(view, 0xd),
333
        itemSpawnBehavior: readUint8(view, 0x10),
334
        stageId: readUint16(view, 0x13),
335
        startingTimerSeconds: readUint32(view, 0x15),
336
        enabledItems: getEnabledItems(view),
337
        players: [0, 1, 2, 3].map(getPlayerObject),
338
        scene: readUint8(view, 0x1a3),
339
        gameMode: readUint8(view, 0x1a4),
340
        language: readUint8(view, 0x2bd),
341
        gameInfoBlock: getGameInfoBlock(view),
342
        randomSeed: readUint32(view, 0x13d),
343
        isPAL: readBool(view, 0x1a1),
344
        isFrozenPS: readBool(view, 0x1a2),
345
        matchInfo: {
346
          sessionId: sessionId,
347
          gameNumber: readUint32(view, 0x2f1),
348
          tiebreakerNumber: readUint32(view, 0x2f5),
349
          /** remove in v8 */
350
          matchId: sessionId,
351
        },
352
      };
353
      return gameSettings;
81✔
354
    case Command.FRAME_START:
355
      return {
266,081✔
356
        frame: readInt32(view, 0x1),
357
        seed: readUint32(view, 0x5),
358
        sceneFrameCounter: readUint32(view, 0x9),
359
      };
360

361
    case Command.PRE_FRAME_UPDATE:
362
      return {
666,153✔
363
        frame: readInt32(view, 0x1),
364
        playerIndex: readUint8(view, 0x5),
365
        isFollower: readBool(view, 0x6),
366
        seed: readUint32(view, 0x7),
367
        actionStateId: readUint16(view, 0xb),
368
        positionX: readFloat(view, 0xd),
369
        positionY: readFloat(view, 0x11),
370
        facingDirection: readFloat(view, 0x15),
371
        joystickX: readFloat(view, 0x19),
372
        joystickY: readFloat(view, 0x1d),
373
        cStickX: readFloat(view, 0x21),
374
        cStickY: readFloat(view, 0x25),
375
        trigger: readFloat(view, 0x29),
376
        buttons: readUint32(view, 0x2d),
377
        physicalButtons: readUint16(view, 0x31),
378
        physicalLTrigger: readFloat(view, 0x33),
379
        physicalRTrigger: readFloat(view, 0x37),
380
        rawJoystickX: readInt8(view, 0x3b),
381
        percent: readFloat(view, 0x3c),
382
      };
383
    case Command.POST_FRAME_UPDATE:
384
      const selfInducedSpeeds: SelfInducedSpeedsType = {
666,152✔
385
        airX: readFloat(view, 0x35),
386
        y: readFloat(view, 0x39),
387
        attackX: readFloat(view, 0x3d),
388
        attackY: readFloat(view, 0x41),
389
        groundX: readFloat(view, 0x45),
390
      };
391
      return {
666,152✔
392
        frame: readInt32(view, 0x1),
393
        playerIndex: readUint8(view, 0x5),
394
        isFollower: readBool(view, 0x6),
395
        internalCharacterId: readUint8(view, 0x7),
396
        actionStateId: readUint16(view, 0x8),
397
        positionX: readFloat(view, 0xa),
398
        positionY: readFloat(view, 0xe),
399
        facingDirection: readFloat(view, 0x12),
400
        percent: readFloat(view, 0x16),
401
        shieldSize: readFloat(view, 0x1a),
402
        lastAttackLanded: readUint8(view, 0x1e),
403
        currentComboCount: readUint8(view, 0x1f),
404
        lastHitBy: readUint8(view, 0x20),
405
        stocksRemaining: readUint8(view, 0x21),
406
        actionStateCounter: readFloat(view, 0x22),
407
        miscActionState: readFloat(view, 0x2b),
408
        isAirborne: readBool(view, 0x2f),
409
        lastGroundId: readUint16(view, 0x30),
410
        jumpsRemaining: readUint8(view, 0x32),
411
        lCancelStatus: readUint8(view, 0x33),
412
        hurtboxCollisionState: readUint8(view, 0x34),
413
        selfInducedSpeeds: selfInducedSpeeds,
414
        hitlagRemaining: readFloat(view, 0x49),
415
        animationIndex: readUint32(view, 0x4d),
416
        instanceHitBy: readUint16(view, 0x51),
417
        instanceId: readUint16(view, 0x53),
418
      };
419
    case Command.ITEM_UPDATE:
420
      return {
37,270✔
421
        frame: readInt32(view, 0x1),
422
        typeId: readUint16(view, 0x5),
423
        state: readUint8(view, 0x7),
424
        facingDirection: readFloat(view, 0x8),
425
        velocityX: readFloat(view, 0xc),
426
        velocityY: readFloat(view, 0x10),
427
        positionX: readFloat(view, 0x14),
428
        positionY: readFloat(view, 0x18),
429
        damageTaken: readUint16(view, 0x1c),
430
        expirationTimer: readFloat(view, 0x1e),
431
        spawnId: readUint32(view, 0x22),
432
        missileType: readUint8(view, 0x26),
433
        turnipFace: readUint8(view, 0x27),
434
        chargeShotLaunched: readUint8(view, 0x28),
435
        chargePower: readUint8(view, 0x29),
436
        owner: readInt8(view, 0x2a),
437
        instanceId: readUint16(view, 0x2b),
438
      };
439
    case Command.FRAME_BOOKEND:
440
      return {
266,081✔
441
        frame: readInt32(view, 0x1),
442
        latestFinalizedFrame: readInt32(view, 0x5),
443
      };
444
    case Command.GAME_END:
445
      const placements = [0, 1, 2, 3].map((playerIndex): PlacementType => {
65✔
446
        const position = readInt8(view, 0x3 + playerIndex);
260✔
447
        return { playerIndex, position };
260✔
448
      });
449

450
      return {
65✔
451
        gameEndMethod: readUint8(view, 0x1),
452
        lrasInitiatorIndex: readInt8(view, 0x2),
453
        placements,
454
      };
455
    case Command.GECKO_LIST:
456
      const codes: GeckoCodeType[] = [];
46✔
457
      let pos = 1;
46✔
458
      while (pos < payload.length) {
46✔
459
        const word1 = readUint32(view, pos) ?? 0;
10,842!
460
        const codetype = (word1 >> 24) & 0xfe;
10,842✔
461
        const address = (word1 & 0x01ffffff) + 0x80000000;
10,842✔
462

463
        let offset = 8; // Default code length, most codes are this length
10,842✔
464
        if (codetype === 0xc0 || codetype === 0xc2) {
10,842✔
465
          const lineCount = readUint32(view, pos + 4) ?? 0;
7,313!
466
          offset = 8 + lineCount * 8;
7,313✔
467
        } else if (codetype === 0x06) {
3,529!
UNCOV
468
          const byteLen = readUint32(view, pos + 4) ?? 0;
×
469
          offset = 8 + ((byteLen + 7) & 0xfffffff8);
×
470
        } else if (codetype === 0x08) {
3,529!
UNCOV
471
          offset = 16;
×
472
        }
473

474
        codes.push({
10,842✔
475
          type: codetype,
476
          address: address,
477
          contents: payload.subarray(pos, pos + offset),
478
        });
479

480
        pos += offset;
10,842✔
481
      }
482

483
      return {
46✔
484
        contents: payload.subarray(1),
485
        codes: codes,
486
      };
487
    case Command.FOD_PLATFORM:
488
      return {
589✔
489
        frame: readInt32(view, 0x1),
490
        platform: readInt8(view, 0x5),
491
        height: readFloat(view, 0x6),
492
      };
493
    case Command.WHISPY:
494
      return {
15✔
495
        frame: readInt32(view, 0x1),
496
        direction: readInt8(view, 0x5),
497
      };
498
    case Command.STADIUM_TRANSFORMATION:
499
      return {
48✔
500
        frame: readInt32(view, 0x1),
501
        event: readUint16(view, 0x5),
502
        transformation: readUint16(view, 0x7),
503
      };
504
    default:
505
      return undefined;
4,468✔
506
  }
507
}
508

509
function canReadFromView(view: DataView, offset: number, length: number): boolean {
510
  const viewLength = view.byteLength;
34,647,956✔
511
  return offset + length <= viewLength;
34,647,956✔
512
}
513

514
function readFloat(view: DataView, offset: number): number | undefined {
515
  if (!canReadFromView(view, offset, 4)) {
16,212,921✔
516
    return undefined;
1,126,244✔
517
  }
518

519
  return view.getFloat32(offset);
15,086,677✔
520
}
521

522
function readInt32(view: DataView, offset: number): number | undefined {
523
  if (!canReadFromView(view, offset, 4)) {
2,168,470!
NEW
UNCOV
524
    return undefined;
×
525
  }
526

527
  return view.getInt32(offset);
2,168,470✔
528
}
529

530
function readInt8(view: DataView, offset: number): number | undefined {
531
  if (!canReadFromView(view, offset, 1)) {
704,433✔
532
    return undefined;
51,001✔
533
  }
534

535
  return view.getInt8(offset);
653,432✔
536
}
537

538
function readUint32(view: DataView, offset: number): number | undefined {
539
  if (!canReadFromView(view, offset, 4)) {
2,587,017✔
540
    return undefined;
703,045✔
541
  }
542

543
  return view.getUint32(offset);
1,883,972✔
544
}
545

546
function readUint16(view: DataView, offset: number): number | undefined {
547
  if (!canReadFromView(view, offset, 2)) {
4,113,024✔
548
    return undefined;
1,251,141✔
549
  }
550

551
  return view.getUint16(offset);
2,861,883✔
552
}
553

554
function readUint8(view: DataView, offset: number, bitmask = 0xff): number | undefined {
6,856,433✔
555
  if (!canReadFromView(view, offset, 1)) {
6,859,268✔
556
    return undefined;
308,292✔
557
  }
558

559
  return view.getUint8(offset) & bitmask;
6,550,976✔
560
}
561

562
function readBool(view: DataView, offset: number): boolean | undefined {
563
  if (!canReadFromView(view, offset, 1)) {
2,002,823✔
564
    return undefined;
101,224✔
565
  }
566

567
  return !!view.getUint8(offset);
1,901,599✔
568
}
569

570
export function getMetadata(slpFile: SlpFileType): MetadataType | undefined {
12✔
571
  if (slpFile.metadataLength <= 0) {
16✔
572
    // This will happen on a severed incomplete file
573
    // $FlowFixMe
574
    return undefined;
2✔
575
  }
576

577
  const buffer = new Uint8Array(slpFile.metadataLength);
14✔
578

579
  slpFile.ref.read(buffer, 0, buffer.length, slpFile.metadataPosition);
14✔
580

581
  let metadata = undefined;
14✔
582
  try {
14✔
583
    metadata = decodeUBJSON(buffer.buffer);
14✔
584
  } catch (ex) {
585
    // Do nothing
586
    // console.log(ex);
587
  }
588

589
  // $FlowFixMe
590
  return metadata;
14✔
591
}
592

593
export function getGameEnd(slpFile: SlpFileType): GameEndType | undefined {
12✔
594
  const { ref, rawDataPosition, rawDataLength, messageSizes } = slpFile;
8✔
595
  const gameEndPayloadSize = messageSizes[Command.GAME_END];
8✔
596
  if (!exists(gameEndPayloadSize) || gameEndPayloadSize <= 0) {
8!
NEW
UNCOV
597
    return undefined;
×
598
  }
599

600
  // Add one to account for command byte
601
  const gameEndSize = gameEndPayloadSize + 1;
8✔
602
  const gameEndPosition = rawDataPosition + rawDataLength - gameEndSize;
8✔
603

604
  const buffer = new Uint8Array(gameEndSize);
8✔
605
  ref.read(buffer, 0, buffer.length, gameEndPosition);
8✔
606
  if (buffer[0] !== Command.GAME_END) {
8!
607
    // This isn't even a game end payload
NEW
UNCOV
608
    return undefined;
×
609
  }
610

611
  const gameEndMessage = parseMessage(Command.GAME_END, buffer);
8✔
612
  if (!gameEndMessage) {
8!
NEW
UNCOV
613
    return undefined;
×
614
  }
615

616
  return gameEndMessage as GameEndType;
8✔
617
}
618

619
export function extractFinalPostFrameUpdates(slpFile: SlpFileType): PostFrameUpdateType[] {
12✔
620
  const { ref, rawDataPosition, rawDataLength, messageSizes } = slpFile;
2✔
621

622
  // The following should exist on all replay versions
623
  const postFramePayloadSize = messageSizes[Command.POST_FRAME_UPDATE];
2✔
624
  const gameEndPayloadSize = messageSizes[Command.GAME_END];
2✔
625
  const frameBookendPayloadSize = messageSizes[Command.FRAME_BOOKEND];
2✔
626

627
  // Technically this should not be possible
628
  if (!exists(postFramePayloadSize)) {
2!
UNCOV
629
    return [];
×
630
  }
631

632
  const gameEndSize = gameEndPayloadSize ? gameEndPayloadSize + 1 : 0;
2!
633
  const postFrameSize = postFramePayloadSize + 1;
2✔
634
  const frameBookendSize = frameBookendPayloadSize ? frameBookendPayloadSize + 1 : 0;
2!
635

636
  let frameNum: number | undefined = undefined;
2✔
637
  let postFramePosition = rawDataPosition + rawDataLength - gameEndSize - frameBookendSize - postFrameSize;
2✔
638
  const postFrameUpdates: PostFrameUpdateType[] = [];
2✔
639
  do {
2✔
640
    const buffer = new Uint8Array(postFrameSize);
7✔
641
    ref.read(buffer, 0, buffer.length, postFramePosition);
7✔
642
    if (buffer[0] !== Command.POST_FRAME_UPDATE) {
7✔
643
      break;
2✔
644
    }
645

646
    const postFrameMessage = parseMessage(Command.POST_FRAME_UPDATE, buffer) as PostFrameUpdateType | undefined;
5✔
647
    if (!postFrameMessage) {
5!
UNCOV
648
      break;
×
649
    }
650

651
    if (frameNum == null) {
5✔
652
      frameNum = postFrameMessage.frame;
2✔
653
    } else if (frameNum !== postFrameMessage.frame) {
3!
654
      // If post frame message is found but the frame doesn't match, it's not part of the final frame
UNCOV
655
      break;
×
656
    }
657

658
    postFrameUpdates.unshift(postFrameMessage);
5✔
659
    postFramePosition -= postFrameSize;
5✔
660
  } while (postFramePosition >= rawDataPosition);
661

662
  return postFrameUpdates;
2✔
663
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc