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

project-slippi / slippi-js / 4405904081

pending completion
4405904081

Pull #123

github

GitHub
Merge 73299c774 into cd8177915
Pull Request #123: [PR] Correctly Calculate Winners for Timeout

645 of 818 branches covered (78.85%)

Branch coverage included in aggregate %.

62 of 62 new or added lines in 3 files covered. (100.0%)

1794 of 2108 relevant lines covered (85.1%)

110071.31 hits per line

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

86.96
/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, isUndefined, isNull } from "lodash";
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
      return (ref as SlpBufferSourceRef).buffer.copy(buffer, offset, position, position + length);
18,633✔
83
    default:
84
      throw new Error("Source type not supported");
×
85
  }
86
}
87

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

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

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

112
  return {
116✔
113
    ref,
114
    rawDataPosition,
115
    rawDataLength,
116
    metadataPosition,
117
    metadataLength,
118
    messageSizes,
119
  };
120
}
121

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

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

135
  if (buffer[0] === 0x36) {
116!
136
    return 0;
×
137
  }
138

139
  if (buffer[0] !== "{".charCodeAt(0)) {
116✔
140
    return 0; // return error?
6✔
141
  }
142

143
  return 15;
110✔
144
}
145

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

152
  const buffer = new Uint8Array(4);
110✔
153
  readRef(ref, buffer, 0, buffer.length, position - 4);
110✔
154

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

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

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

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

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

196
  const payloadLength = buffer[1] as number;
110✔
197
  (messageSizes[0x35] as any) = payloadLength;
110✔
198

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

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

208
  return messageSizes;
110✔
209
}
210

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

218
  return enabledItems;
75✔
219
}
220

221
function getGameInfoBlock(view: DataView): GameInfoType {
222
  const offset = 0x5;
75✔
223

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

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

253
  let readPosition = startPos !== null && startPos > 0 ? startPos : slpFile.rawDataPosition;
91✔
254
  const stopReadingAt = slpFile.rawDataPosition + slpFile.rawDataLength;
91✔
255

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

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

270
    if (buffer.length > stopReadingAt - readPosition) {
1,002,458!
271
      return readPosition;
×
272
    }
273

274
    const advanceAmount = buffer.length;
1,002,458✔
275

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

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

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

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

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

313
    readPosition += advanceAmount;
1,002,432✔
314
  }
315

316
  return readPosition;
73✔
317
}
318

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

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

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

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

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

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

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

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

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

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

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

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

567
        pos += offset;
9,430✔
568
      }
569

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

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

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

589
  return view.getFloat32(offset);
12,497,688✔
590
}
591

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

597
  return view.getInt32(offset);
1,789,370✔
598
}
599

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

605
  return view.getInt8(offset);
539,119✔
606
}
607

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

613
  return view.getUint32(offset);
1,450,161✔
614
}
615

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

621
  return view.getUint16(offset);
2,202,335✔
622
}
623

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

629
  return view.getUint8(offset) & bitmask;
5,451,729✔
630
}
631

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

637
  return !!view.getUint8(offset);
1,583,394✔
638
}
639

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

647
  const buffer = new Uint8Array(slpFile.metadataLength);
14✔
648

649
  readRef(slpFile.ref, buffer, 0, buffer.length, slpFile.metadataPosition);
14✔
650

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

659
  // $FlowFixMe
660
  return metadata;
14✔
661
}
662

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

670
  // Add one to account for command byte
671
  const gameEndSize = gameEndPayloadSize + 1;
8✔
672
  const gameEndPosition = rawDataPosition + rawDataLength - gameEndSize;
8✔
673

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

681
  const gameEndMessage = parseMessage(Command.GAME_END, buffer);
8✔
682
  if (!gameEndMessage) {
8!
683
    return null;
×
684
  }
685

686
  return gameEndMessage as GameEndType;
8✔
687
}
688

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

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

697
  // Technically this should not be possible
698
  if (isUndefined(postFramePayloadSize)) {
2!
699
    return [];
×
700
  }
701

702
  const gameEndSize = gameEndPayloadSize ? gameEndPayloadSize + 1 : 0;
2!
703
  const postFrameSize = postFramePayloadSize + 1;
2✔
704
  const frameBookendSize = frameBookendPayloadSize ? frameBookendPayloadSize + 1 : 0;
2!
705

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

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

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

728
    postFrameUpdates.unshift(postFrameMessage);
5✔
729
    postFramePosition -= postFrameSize;
5✔
730
  } while (postFramePosition >= rawDataPosition);
731

732
  return postFrameUpdates;
2✔
733
}
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