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

project-slippi / slippi-js / 5743611105

pending completion
5743611105

push

github

JLaferri
fix: make oob buffer read match file read

without this, the buffer copy would error whereas for a file it wouldn't

647 of 821 branches covered (78.81%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

1815 of 2130 relevant lines covered (85.21%)

108943.27 hits per line

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

86.55
/src/utils/slpReader.ts
1
import { decode } from "@shelacek/ubjson";
11✔
2
import fs from "fs";
11✔
3
import iconv from "iconv-lite";
11✔
4
import mapValues from "lodash/mapValues";
11✔
5

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

22
export enum SlpInputSource {
11✔
23
  BUFFER = "buffer",
11✔
24
  FILE = "file",
11✔
25
}
26

27
export interface SlpReadInput {
28
  source: SlpInputSource;
29
  filePath?: string;
30
  buffer?: Buffer;
31
}
32

33
export type SlpRefType = SlpFileSourceRef | SlpBufferSourceRef;
34

35
export interface SlpFileType {
36
  ref: SlpRefType;
37
  rawDataPosition: number;
38
  rawDataLength: number;
39
  metadataPosition: number;
40
  metadataLength: number;
41
  messageSizes: {
42
    [command: number]: number;
43
  };
44
}
45

46
export interface SlpFileSourceRef {
47
  source: SlpInputSource;
48
  fileDescriptor: number;
49
}
50

51
export interface SlpBufferSourceRef {
52
  source: SlpInputSource;
53
  buffer: Buffer;
54
}
55

56
function getRef(input: SlpReadInput): SlpRefType {
57
  switch (input.source) {
116!
58
    case SlpInputSource.FILE:
59
      if (!input.filePath) {
86!
60
        throw new Error("File source requires a file path");
×
61
      }
62
      const fd = fs.openSync(input.filePath, "r");
86✔
63
      return {
86✔
64
        source: input.source,
65
        fileDescriptor: fd,
66
      } as SlpFileSourceRef;
67
    case SlpInputSource.BUFFER:
68
      return {
30✔
69
        source: input.source,
70
        buffer: input.buffer,
71
      } as SlpBufferSourceRef;
72
    default:
73
      throw new Error("Source type not supported");
×
74
  }
75
}
76

77
function readRef(ref: SlpRefType, buffer: Uint8Array, offset: number, length: number, position: number): number {
78
  switch (ref.source) {
2,005,409!
79
    case SlpInputSource.FILE:
80
      return fs.readSync((ref as SlpFileSourceRef).fileDescriptor, buffer, offset, length, position);
1,986,776✔
81
    case SlpInputSource.BUFFER:
82
      if (position >= (ref as SlpBufferSourceRef).buffer.length) {
18,633!
83
        return 0;
×
84
      }
85
      return (ref as SlpBufferSourceRef).buffer.copy(buffer, offset, position, position + length);
18,633✔
86
    default:
87
      throw new Error("Source type not supported");
×
88
  }
89
}
90

91
function getLenRef(ref: SlpRefType): number {
92
  switch (ref.source) {
232!
93
    case SlpInputSource.FILE:
94
      const fileStats = fs.fstatSync((ref as SlpFileSourceRef).fileDescriptor);
172✔
95
      return fileStats.size;
172✔
96
    case SlpInputSource.BUFFER:
97
      return (ref as SlpBufferSourceRef).buffer.length;
60✔
98
    default:
99
      throw new Error("Source type not supported");
×
100
  }
101
}
102

103
/**
104
 * Opens a file at path
105
 */
106
export function openSlpFile(input: SlpReadInput): SlpFileType {
11✔
107
  const ref = getRef(input);
116✔
108

109
  const rawDataPosition = getRawDataPosition(ref);
116✔
110
  const rawDataLength = getRawDataLength(ref, rawDataPosition);
116✔
111
  const metadataPosition = rawDataPosition + rawDataLength + 10; // remove metadata string
116✔
112
  const metadataLength = getMetadataLength(ref, metadataPosition);
116✔
113
  const messageSizes = getMessageSizes(ref, rawDataPosition);
116✔
114

115
  return {
116✔
116
    ref,
117
    rawDataPosition,
118
    rawDataLength,
119
    metadataPosition,
120
    metadataLength,
121
    messageSizes,
122
  };
123
}
124

125
export function closeSlpFile(file: SlpFileType): void {
11✔
126
  switch (file.ref.source) {
112✔
127
    case SlpInputSource.FILE:
128
      fs.closeSync((file.ref as SlpFileSourceRef).fileDescriptor);
82✔
129
      break;
82✔
130
  }
131
}
132

133
// This function gets the position where the raw data starts
134
function getRawDataPosition(ref: SlpRefType): number {
135
  const buffer = new Uint8Array(1);
116✔
136
  readRef(ref, buffer, 0, buffer.length, 0);
116✔
137

138
  if (buffer[0] === 0x36) {
116!
139
    return 0;
×
140
  }
141

142
  if (buffer[0] !== "{".charCodeAt(0)) {
116✔
143
    return 0; // return error?
6✔
144
  }
145

146
  return 15;
110✔
147
}
148

149
function getRawDataLength(ref: SlpRefType, position: number): number {
150
  const fileSize = getLenRef(ref);
116✔
151
  if (position === 0) {
116✔
152
    return fileSize;
6✔
153
  }
154

155
  const buffer = new Uint8Array(4);
110✔
156
  readRef(ref, buffer, 0, buffer.length, position - 4);
110✔
157

158
  const rawDataLen = (buffer[0]! << 24) | (buffer[1]! << 16) | (buffer[2]! << 8) | buffer[3]!;
110✔
159
  if (rawDataLen > 0) {
110✔
160
    // If this method manages to read a number, it's probably trustworthy
161
    return rawDataLen;
107✔
162
  }
163

164
  // If the above does not return a valid data length,
165
  // return a file size based on file length. This enables
166
  // some support for severed files
167
  return fileSize - position;
3✔
168
}
169

170
function getMetadataLength(ref: SlpRefType, position: number): number {
171
  const len = getLenRef(ref);
116✔
172
  return len - position - 1;
116✔
173
}
174

175
function getMessageSizes(
176
  ref: SlpRefType,
177
  position: number,
178
): {
179
  [command: number]: number;
180
} {
181
  const messageSizes: {
182
    [command: number]: number;
183
  } = {};
116✔
184
  // Support old file format
185
  if (position === 0) {
116✔
186
    messageSizes[0x36] = 0x140;
6✔
187
    messageSizes[0x37] = 0x6;
6✔
188
    messageSizes[0x38] = 0x46;
6✔
189
    messageSizes[0x39] = 0x1;
6✔
190
    return messageSizes;
6✔
191
  }
192

193
  const buffer = new Uint8Array(2);
110✔
194
  readRef(ref, buffer, 0, buffer.length, position);
110✔
195
  if (buffer[0] !== Command.MESSAGE_SIZES) {
110!
196
    return {};
×
197
  }
198

199
  const payloadLength = buffer[1] as number;
110✔
200
  (messageSizes[0x35] as any) = payloadLength;
110✔
201

202
  const messageSizesBuffer = new Uint8Array(payloadLength - 1);
110✔
203
  readRef(ref, messageSizesBuffer, 0, messageSizesBuffer.length, position + 2);
110✔
204
  for (let i = 0; i < payloadLength - 1; i += 3) {
110✔
205
    const command = messageSizesBuffer[i] as number;
765✔
206

207
    // Get size of command
208
    (messageSizes[command] as any) = (messageSizesBuffer[i + 1]! << 8) | messageSizesBuffer[i + 2]!;
765✔
209
  }
210

211
  return messageSizes;
110✔
212
}
213

214
function getEnabledItems(view: DataView): number {
215
  const offsets = [0x1, 0x100, 0x10000, 0x1000000, 0x100000000];
75✔
216
  const enabledItems = offsets.reduce((acc, byteOffset, index) => {
75✔
217
    const byte = readUint8(view, 0x28 + index) as number;
375✔
218
    return acc + byte * byteOffset;
375✔
219
  }, 0);
220

221
  return enabledItems;
75✔
222
}
223

224
function getGameInfoBlock(view: DataView): GameInfoType {
225
  const offset = 0x5;
75✔
226

227
  return {
75✔
228
    gameBitfield1: readUint8(view, 0x0 + offset),
229
    gameBitfield2: readUint8(view, 0x1 + offset),
230
    gameBitfield3: readUint8(view, 0x2 + offset),
231
    gameBitfield4: readUint8(view, 0x3 + offset),
232
    bombRainEnabled: (readUint8(view, 0x6 + offset)! & 0xff) > 0 ? true : false,
75!
233
    itemSpawnBehavior: readInt8(view, 0xb + offset),
234
    selfDestructScoreValue: readInt8(view, 0xc + offset),
235
    //stageId: readUint16(view, 0xe + offset),
236
    //gameTimer: readUint32(view, 0x10 + offset),
237
    itemSpawnBitfield1: readUint8(view, 0x23 + offset),
238
    itemSpawnBitfield2: readUint8(view, 0x24 + offset),
239
    itemSpawnBitfield3: readUint8(view, 0x25 + offset),
240
    itemSpawnBitfield4: readUint8(view, 0x26 + offset),
241
    itemSpawnBitfield5: readUint8(view, 0x27 + offset),
242
    damageRatio: readFloat(view, 0x30 + offset),
243
  } as GameInfoType;
244
}
245

246
/**
247
 * Iterates through slp events and parses payloads
248
 */
249
export function iterateEvents(
11✔
250
  slpFile: SlpFileType,
251
  callback: EventCallbackFunc,
252
  startPos: number | null = null,
×
253
): number {
254
  const ref = slpFile.ref;
91✔
255

256
  let readPosition = startPos !== null && startPos > 0 ? startPos : slpFile.rawDataPosition;
91✔
257
  const stopReadingAt = slpFile.rawDataPosition + slpFile.rawDataLength;
91✔
258

259
  // Generate read buffers for each
260
  const commandPayloadBuffers = mapValues(slpFile.messageSizes, (size) => new Uint8Array(size + 1));
710✔
261
  let splitMessageBuffer = new Uint8Array(0);
91✔
262

263
  const commandByteBuffer = new Uint8Array(1);
91✔
264
  while (readPosition < stopReadingAt) {
91✔
265
    readRef(ref, commandByteBuffer, 0, 1, readPosition);
1,002,476✔
266
    let commandByte = (commandByteBuffer[0] as number) ?? 0;
1,002,476!
267
    let buffer = commandPayloadBuffers[commandByte];
1,002,476✔
268
    if (buffer === undefined) {
1,002,476✔
269
      // If we don't have an entry for this command, return false to indicate failed read
270
      return readPosition;
18✔
271
    }
272

273
    if (buffer.length > stopReadingAt - readPosition) {
1,002,458!
274
      return readPosition;
×
275
    }
276

277
    const advanceAmount = buffer.length;
1,002,458✔
278

279
    readRef(ref, buffer, 0, buffer.length, readPosition);
1,002,458✔
280
    if (commandByte === Command.SPLIT_MESSAGE) {
1,002,458✔
281
      // Here we have a split message, we will collect data from them until the last
282
      // message of the list is received
283
      const view = new DataView(buffer.buffer);
3,570✔
284
      const size = readUint16(view, 0x201) ?? 512;
3,570!
285
      const isLastMessage = readBool(view, 0x204);
3,570✔
286
      const internalCommand = readUint8(view, 0x203) ?? 0;
3,570!
287

288
      // If this is the first message, initialize the splitMessageBuffer
289
      // with the internal command byte because our parseMessage function
290
      // seems to expect a command byte at the start
291
      if (splitMessageBuffer.length === 0) {
3,570✔
292
        splitMessageBuffer = new Uint8Array(1);
41✔
293
        splitMessageBuffer[0] = internalCommand;
41✔
294
      }
295

296
      // Collect new data into splitMessageBuffer
297
      const appendBuf = buffer.slice(0x1, 0x1 + size);
3,570✔
298
      const mergedBuf = new Uint8Array(splitMessageBuffer.length + appendBuf.length);
3,570✔
299
      mergedBuf.set(splitMessageBuffer);
3,570✔
300
      mergedBuf.set(appendBuf, splitMessageBuffer.length);
3,570✔
301
      splitMessageBuffer = mergedBuf;
3,570✔
302

303
      if (isLastMessage) {
3,570✔
304
        commandByte = splitMessageBuffer[0] ?? 0;
41!
305
        buffer = splitMessageBuffer;
41✔
306
        splitMessageBuffer = new Uint8Array(0);
41✔
307
      }
308
    }
309

310
    const parsedPayload = parseMessage(commandByte, buffer);
1,002,458✔
311
    const shouldStop = callback(commandByte, parsedPayload, buffer);
1,002,458✔
312
    if (shouldStop) {
1,002,458✔
313
      break;
26✔
314
    }
315

316
    readPosition += advanceAmount;
1,002,432✔
317
  }
318

319
  return readPosition;
73✔
320
}
321

322
export function parseMessage(command: Command, payload: Uint8Array): EventPayloadTypes | null {
11✔
323
  const view = new DataView(payload.buffer);
1,580,318✔
324
  switch (command) {
1,580,318✔
325
    case Command.GAME_START:
326
      const getPlayerObject = (playerIndex: number): PlayerType => {
75✔
327
        // Controller Fix stuff
328
        const cfOffset = playerIndex * 0x8;
300✔
329
        const dashback = readUint32(view, 0x141 + cfOffset);
300✔
330
        const shieldDrop = readUint32(view, 0x145 + cfOffset);
300✔
331
        let controllerFix = "None";
300✔
332
        if (dashback !== shieldDrop) {
300!
333
          controllerFix = "Mixed";
×
334
        } else if (dashback === 1) {
300✔
335
          controllerFix = "UCF";
195✔
336
        } else if (dashback === 2) {
105✔
337
          controllerFix = "Dween";
1✔
338
        }
339

340
        // Nametag stuff
341
        const nametagLength = 0x10;
300✔
342
        const nametagOffset = playerIndex * nametagLength;
300✔
343
        const nametagStart = 0x161 + nametagOffset;
300✔
344
        const nametagBuf = payload.slice(nametagStart, nametagStart + nametagLength);
300✔
345
        const nameTagString = iconv
300✔
346
          .decode(nametagBuf as Buffer, "Shift_JIS")
347
          .split("\0")
348
          .shift();
349
        const nametag = nameTagString ? toHalfwidth(nameTagString) : "";
300✔
350

351
        // Display name
352
        const displayNameLength = 0x1f;
300✔
353
        const displayNameOffset = playerIndex * displayNameLength;
300✔
354
        const displayNameStart = 0x1a5 + displayNameOffset;
300✔
355
        const displayNameBuf = payload.slice(displayNameStart, displayNameStart + displayNameLength);
300✔
356
        const displayNameString = iconv
300✔
357
          .decode(displayNameBuf as Buffer, "Shift_JIS")
358
          .split("\0")
359
          .shift();
360
        const displayName = displayNameString ? toHalfwidth(displayNameString) : "";
300✔
361

362
        // Connect code
363
        const connectCodeLength = 0xa;
300✔
364
        const connectCodeOffset = playerIndex * connectCodeLength;
300✔
365
        const connectCodeStart = 0x221 + connectCodeOffset;
300✔
366
        const connectCodeBuf = payload.slice(connectCodeStart, connectCodeStart + connectCodeLength);
300✔
367
        const connectCodeString = iconv
300✔
368
          .decode(connectCodeBuf as Buffer, "Shift_JIS")
369
          .split("\0")
370
          .shift();
371
        const connectCode = connectCodeString ? toHalfwidth(connectCodeString) : "";
300✔
372

373
        const userIdLength = 0x1d;
300✔
374
        const userIdOffset = playerIndex * userIdLength;
300✔
375
        const userIdStart = 0x249 + userIdOffset;
300✔
376
        const userIdBuf = payload.slice(userIdStart, userIdStart + userIdLength);
300✔
377
        const userIdString = iconv
300✔
378
          .decode(userIdBuf as Buffer, "utf8")
379
          .split("\0")
380
          .shift();
381
        const userId = userIdString ?? "";
300!
382

383
        const offset = playerIndex * 0x24;
300✔
384
        return {
300✔
385
          playerIndex,
386
          port: playerIndex + 1,
387
          characterId: readUint8(view, 0x65 + offset),
388
          type: readUint8(view, 0x66 + offset),
389
          startStocks: readUint8(view, 0x67 + offset),
390
          characterColor: readUint8(view, 0x68 + offset),
391
          teamShade: readUint8(view, 0x6c + offset),
392
          handicap: readUint8(view, 0x6d + offset),
393
          teamId: readUint8(view, 0x6e + offset),
394
          staminaMode: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x01)),
395
          silentCharacter: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x02)),
396
          lowGravity: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x04)),
397
          invisible: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x08)),
398
          blackStockIcon: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x10)),
399
          metal: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x20)),
400
          startOnAngelPlatform: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x40)),
401
          rumbleEnabled: Boolean(readUint8(view, 0x6c + playerIndex * 0x24, 0x80)),
402
          cpuLevel: readUint8(view, 0x74 + offset),
403
          offenseRatio: readFloat(view, 0x7d + offset),
404
          defenseRatio: readFloat(view, 0x81 + offset),
405
          modelScale: readFloat(view, 0x85 + offset),
406
          controllerFix,
407
          nametag,
408
          displayName,
409
          connectCode,
410
          userId,
411
        };
412
      };
413

414
      const matchIdLength = 51;
75✔
415
      const matchIdStart = 0x2be;
75✔
416
      const matchIdBuf = payload.slice(matchIdStart, matchIdStart + matchIdLength);
75✔
417
      const matchIdString = iconv
75✔
418
        .decode(matchIdBuf as Buffer, "utf8")
419
        .split("\0")
420
        .shift();
421
      const matchId = matchIdString ?? "";
75!
422

423
      return {
75✔
424
        slpVersion: `${readUint8(view, 0x1)}.${readUint8(view, 0x2)}.${readUint8(view, 0x3)}`,
425
        timerType: readUint8(view, 0x5, 0x03),
426
        inGameMode: readUint8(view, 0x5, 0xe0),
427
        friendlyFireEnabled: !!readUint8(view, 0x6, 0x01),
428
        isTeams: readBool(view, 0xd),
429
        itemSpawnBehavior: readUint8(view, 0x10),
430
        stageId: readUint16(view, 0x13),
431
        startingTimerSeconds: readUint32(view, 0x15),
432
        enabledItems: getEnabledItems(view),
433
        players: [0, 1, 2, 3].map(getPlayerObject),
434
        scene: readUint8(view, 0x1a3),
435
        gameMode: readUint8(view, 0x1a4),
436
        language: readUint8(view, 0x2bd),
437
        gameInfoBlock: getGameInfoBlock(view),
438
        randomSeed: readUint32(view, 0x13d),
439
        isPAL: readBool(view, 0x1a1),
440
        isFrozenPS: readBool(view, 0x1a2),
441
        matchInfo: {
442
          matchId,
443
          gameNumber: readUint32(view, 0x2f1),
444
          tiebreakerNumber: readUint32(view, 0x2f5),
445
        },
446
      };
447
    case Command.FRAME_START:
448
      return {
213,142✔
449
        frame: readInt32(view, 0x1),
450
        seed: readUint32(view, 0x5),
451
        sceneFrameCounter: readUint32(view, 0x9),
452
      };
453

454
    case Command.PRE_FRAME_UPDATE:
455
      return {
560,275✔
456
        frame: readInt32(view, 0x1),
457
        playerIndex: readUint8(view, 0x5),
458
        isFollower: readBool(view, 0x6),
459
        seed: readUint32(view, 0x7),
460
        actionStateId: readUint16(view, 0xb),
461
        positionX: readFloat(view, 0xd),
462
        positionY: readFloat(view, 0x11),
463
        facingDirection: readFloat(view, 0x15),
464
        joystickX: readFloat(view, 0x19),
465
        joystickY: readFloat(view, 0x1d),
466
        cStickX: readFloat(view, 0x21),
467
        cStickY: readFloat(view, 0x25),
468
        trigger: readFloat(view, 0x29),
469
        buttons: readUint32(view, 0x2d),
470
        physicalButtons: readUint16(view, 0x31),
471
        physicalLTrigger: readFloat(view, 0x33),
472
        physicalRTrigger: readFloat(view, 0x37),
473
        rawJoystickX: readInt8(view, 0x3b),
474
        percent: readFloat(view, 0x3c),
475
      };
476
    case Command.POST_FRAME_UPDATE:
477
      const selfInducedSpeeds: SelfInducedSpeedsType = {
560,274✔
478
        airX: readFloat(view, 0x35),
479
        y: readFloat(view, 0x39),
480
        attackX: readFloat(view, 0x3d),
481
        attackY: readFloat(view, 0x41),
482
        groundX: readFloat(view, 0x45),
483
      };
484
      return {
560,274✔
485
        frame: readInt32(view, 0x1),
486
        playerIndex: readUint8(view, 0x5),
487
        isFollower: readBool(view, 0x6),
488
        internalCharacterId: readUint8(view, 0x7),
489
        actionStateId: readUint16(view, 0x8),
490
        positionX: readFloat(view, 0xa),
491
        positionY: readFloat(view, 0xe),
492
        facingDirection: readFloat(view, 0x12),
493
        percent: readFloat(view, 0x16),
494
        shieldSize: readFloat(view, 0x1a),
495
        lastAttackLanded: readUint8(view, 0x1e),
496
        currentComboCount: readUint8(view, 0x1f),
497
        lastHitBy: readUint8(view, 0x20),
498
        stocksRemaining: readUint8(view, 0x21),
499
        actionStateCounter: readFloat(view, 0x22),
500
        miscActionState: readFloat(view, 0x2b),
501
        isAirborne: readBool(view, 0x2f),
502
        lastGroundId: readUint16(view, 0x30),
503
        jumpsRemaining: readUint8(view, 0x32),
504
        lCancelStatus: readUint8(view, 0x33),
505
        hurtboxCollisionState: readUint8(view, 0x34),
506
        selfInducedSpeeds: selfInducedSpeeds,
507
        hitlagRemaining: readFloat(view, 0x49),
508
        animationIndex: readUint32(view, 0x4d),
509
      };
510
    case Command.ITEM_UPDATE:
511
      return {
29,395✔
512
        frame: readInt32(view, 0x1),
513
        typeId: readUint16(view, 0x5),
514
        state: readUint8(view, 0x7),
515
        facingDirection: readFloat(view, 0x8),
516
        velocityX: readFloat(view, 0xc),
517
        velocityY: readFloat(view, 0x10),
518
        positionX: readFloat(view, 0x14),
519
        positionY: readFloat(view, 0x18),
520
        damageTaken: readUint16(view, 0x1c),
521
        expirationTimer: readFloat(view, 0x1e),
522
        spawnId: readUint32(view, 0x22),
523
        missileType: readUint8(view, 0x26),
524
        turnipFace: readUint8(view, 0x27),
525
        chargeShotLaunched: readUint8(view, 0x28),
526
        chargePower: readUint8(view, 0x29),
527
        owner: readInt8(view, 0x2a),
528
      };
529
    case Command.FRAME_BOOKEND:
530
      return {
213,142✔
531
        frame: readInt32(view, 0x1),
532
        latestFinalizedFrame: readInt32(view, 0x5),
533
      };
534
    case Command.GAME_END:
535
      const placements = [0, 1, 2, 3].map((playerIndex): PlacementType => {
60✔
536
        const position = readInt8(view, 0x3 + playerIndex);
240✔
537
        return { playerIndex, position };
240✔
538
      });
539

540
      return {
60✔
541
        gameEndMethod: readUint8(view, 0x1),
542
        lrasInitiatorIndex: readInt8(view, 0x2),
543
        placements,
544
      };
545
    case Command.GECKO_LIST:
546
      const codes: GeckoCodeType[] = [];
41✔
547
      let pos = 1;
41✔
548
      while (pos < payload.length) {
41✔
549
        const word1 = readUint32(view, pos) ?? 0;
9,430!
550
        const codetype = (word1 >> 24) & 0xfe;
9,430✔
551
        const address = (word1 & 0x01ffffff) + 0x80000000;
9,430✔
552

553
        let offset = 8; // Default code length, most codes are this length
9,430✔
554
        if (codetype === 0xc0 || codetype === 0xc2) {
9,430✔
555
          const lineCount = readUint32(view, pos + 4) ?? 0;
6,373!
556
          offset = 8 + lineCount * 8;
6,373✔
557
        } else if (codetype === 0x06) {
3,057!
558
          const byteLen = readUint32(view, pos + 4) ?? 0;
×
559
          offset = 8 + ((byteLen + 7) & 0xfffffff8);
×
560
        } else if (codetype === 0x08) {
3,057!
561
          offset = 16;
×
562
        }
563

564
        codes.push({
9,430✔
565
          type: codetype,
566
          address: address,
567
          contents: payload.slice(pos, pos + offset),
568
        });
569

570
        pos += offset;
9,430✔
571
      }
572

573
      return {
41✔
574
        contents: payload.slice(1),
575
        codes: codes,
576
      };
577
    default:
578
      return null;
3,914✔
579
  }
580
}
581

582
function canReadFromView(view: DataView, offset: number, length: number): boolean {
583
  const viewLength = view.byteLength;
27,904,800✔
584
  return offset + length <= viewLength;
27,904,800✔
585
}
586

587
function readFloat(view: DataView, offset: number): number | null {
588
  if (!canReadFromView(view, offset, 4)) {
13,623,932✔
589
    return null;
1,126,244✔
590
  }
591

592
  return view.getFloat32(offset);
12,497,688✔
593
}
594

595
function readInt32(view: DataView, offset: number): number | null {
596
  if (!canReadFromView(view, offset, 4)) {
1,789,370!
597
    return null;
×
598
  }
599

600
  return view.getInt32(offset);
1,789,370✔
601
}
602

603
function readInt8(view: DataView, offset: number): number | null {
604
  if (!canReadFromView(view, offset, 1)) {
590,120✔
605
    return null;
51,001✔
606
  }
607

608
  return view.getInt8(offset);
539,119✔
609
}
610

611
function readUint32(view: DataView, offset: number): number | null {
612
  if (!canReadFromView(view, offset, 4)) {
2,153,206✔
613
    return null;
703,045✔
614
  }
615

616
  return view.getUint32(offset);
1,450,161✔
617
}
618

619
function readUint16(view: DataView, offset: number): number | null {
620
  if (!canReadFromView(view, offset, 2)) {
2,303,533✔
621
    return null;
101,198✔
622
  }
623

624
  return view.getUint16(offset);
2,202,335✔
625
}
626

627
function readUint8(view: DataView, offset: number, bitmask = 0xff): number | null {
5,757,396✔
628
  if (!canReadFromView(view, offset, 1)) {
5,760,021✔
629
    return null;
308,292✔
630
  }
631

632
  return view.getUint8(offset) & bitmask;
5,451,729✔
633
}
634

635
function readBool(view: DataView, offset: number): boolean | null {
636
  if (!canReadFromView(view, offset, 1)) {
1,684,618✔
637
    return null;
101,224✔
638
  }
639

640
  return !!view.getUint8(offset);
1,583,394✔
641
}
642

643
export function getMetadata(slpFile: SlpFileType): MetadataType | null {
11✔
644
  if (slpFile.metadataLength <= 0) {
16✔
645
    // This will happen on a severed incomplete file
646
    // $FlowFixMe
647
    return null;
2✔
648
  }
649

650
  const buffer = new Uint8Array(slpFile.metadataLength);
14✔
651

652
  readRef(slpFile.ref, buffer, 0, buffer.length, slpFile.metadataPosition);
14✔
653

654
  let metadata = null;
14✔
655
  try {
14✔
656
    metadata = decode(buffer);
14✔
657
  } catch (ex) {
658
    // Do nothing
659
    // console.log(ex);
660
  }
661

662
  // $FlowFixMe
663
  return metadata;
14✔
664
}
665

666
export function getGameEnd(slpFile: SlpFileType): GameEndType | null {
11✔
667
  const { ref, rawDataPosition, rawDataLength, messageSizes } = slpFile;
8✔
668
  const gameEndPayloadSize = messageSizes[Command.GAME_END];
8✔
669
  if (!exists(gameEndPayloadSize) || gameEndPayloadSize <= 0) {
8!
670
    return null;
×
671
  }
672

673
  // Add one to account for command byte
674
  const gameEndSize = gameEndPayloadSize + 1;
8✔
675
  const gameEndPosition = rawDataPosition + rawDataLength - gameEndSize;
8✔
676

677
  const buffer = new Uint8Array(gameEndSize);
8✔
678
  readRef(ref, buffer, 0, buffer.length, gameEndPosition);
8✔
679
  if (buffer[0] !== Command.GAME_END) {
8!
680
    // This isn't even a game end payload
681
    return null;
×
682
  }
683

684
  const gameEndMessage = parseMessage(Command.GAME_END, buffer);
8✔
685
  if (!gameEndMessage) {
8!
686
    return null;
×
687
  }
688

689
  return gameEndMessage as GameEndType;
8✔
690
}
691

692
export function extractFinalPostFrameUpdates(slpFile: SlpFileType): PostFrameUpdateType[] {
11✔
693
  const { ref, rawDataPosition, rawDataLength, messageSizes } = slpFile;
2✔
694

695
  // The following should exist on all replay versions
696
  const postFramePayloadSize = messageSizes[Command.POST_FRAME_UPDATE];
2✔
697
  const gameEndPayloadSize = messageSizes[Command.GAME_END];
2✔
698
  const frameBookendPayloadSize = messageSizes[Command.FRAME_BOOKEND];
2✔
699

700
  // Technically this should not be possible
701
  if (!exists(postFramePayloadSize)) {
2!
702
    return [];
×
703
  }
704

705
  const gameEndSize = gameEndPayloadSize ? gameEndPayloadSize + 1 : 0;
2!
706
  const postFrameSize = postFramePayloadSize + 1;
2✔
707
  const frameBookendSize = frameBookendPayloadSize ? frameBookendPayloadSize + 1 : 0;
2!
708

709
  let frameNum: number | null = null;
2✔
710
  let postFramePosition = rawDataPosition + rawDataLength - gameEndSize - frameBookendSize - postFrameSize;
2✔
711
  const postFrameUpdates: PostFrameUpdateType[] = [];
2✔
712
  do {
2✔
713
    const buffer = new Uint8Array(postFrameSize);
7✔
714
    readRef(ref, buffer, 0, buffer.length, postFramePosition);
7✔
715
    if (buffer[0] !== Command.POST_FRAME_UPDATE) {
7✔
716
      break;
2✔
717
    }
718

719
    const postFrameMessage = parseMessage(Command.POST_FRAME_UPDATE, buffer) as PostFrameUpdateType | null;
5✔
720
    if (!postFrameMessage) {
5!
721
      break;
×
722
    }
723

724
    if (frameNum === null) {
5✔
725
      frameNum = postFrameMessage.frame;
2✔
726
    } else if (frameNum !== postFrameMessage.frame) {
3!
727
      // If post frame message is found but the frame doesn't match, it's not part of the final frame
728
      break;
×
729
    }
730

731
    postFrameUpdates.unshift(postFrameMessage);
5✔
732
    postFramePosition -= postFrameSize;
5✔
733
  } while (postFramePosition >= rawDataPosition);
734

735
  return postFrameUpdates;
2✔
736
}
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