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

project-slippi / slippi-js / 20256270422

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

Pull #158

github

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

696 of 929 branches covered (74.92%)

Branch coverage included in aggregate %.

63 of 82 new or added lines in 20 files covered. (76.83%)

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

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

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

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

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

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

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

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

68
  return 15;
116✔
69
}
70

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

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

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

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

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

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

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

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

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

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

133
  return messageSizes;
116✔
134
}
135

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

143
  return enabledItems;
81✔
144
}
145

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

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

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

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

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

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

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

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

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

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

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

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

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

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

238
  return readPosition;
79✔
239
}
240

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

617
  return gameEndMessage as GameEndType;
8✔
618
}
619

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

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

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

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

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

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

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

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

663
  return postFrameUpdates;
2✔
664
}
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