summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--package.json3
-rw-r--r--src/constants.ts54
-rw-r--r--src/file.ts90
-rw-r--r--src/lib.ts222
-rw-r--r--src/packets/general.ts398
-rw-r--r--src/packets/index.ts58
-rw-r--r--src/packets/input.ts24
-rw-r--r--src/packets/utils.ts37
-rw-r--r--src/utils.ts95
-rw-r--r--tsconfig.json4
11 files changed, 766 insertions, 222 deletions
diff --git a/README.md b/README.md
index 2a2272e..efaa95a 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,9 @@ https://github.com/bigbass1997/TASD-Spec
This library is designed to work in both broswers and nodejs
+## This Library is still in flux much like the spec it is for
+## Anything could change between versions until a stable 1.0.0 is released
+
# Usage
`static TASD.parseFile(buffer: Uint8Array)`
Try to parses a file from a Uint8Array
diff --git a/package.json b/package.json
index 99c59d6..fad77d6 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,8 @@
"license": "MIT",
"files": [
"dist/lib.js",
- "dist/lib.d.ts"
+ "dist/*.d.ts",
+ "!dist/index.d.ts"
],
"repository": {
"type": "git",
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..4ff721a
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,54 @@
+
+export const PACKET_TYPES = {
+ // General Keys
+ 'CONSOLE_TYPE': 1,
+ 'CONSOLE_REGION': 2,
+ 'GAME_TITLE': 3,
+ 'ROM_NAME': 4,
+ 'ATTRIBUTION': 5,
+ 'CATEGORY': 6,
+ 'EMULATOR_NAME': 7,
+ 'EMULATOR_VERSION': 8,
+ 'EMULATOR_CORE': 9,
+ 'TAS_LAST_MODIFIED': 10,
+ 'DUMP_CREATED': 11,
+ 'DUMP_LAST_MODIFIED': 12,
+ 'TOTAL_FRAMES': 13,
+ 'RERECORDS': 14,
+ 'SOURCE_LINK': 15,
+ 'BLANK_FRAMES': 16,
+ 'VERIFIED': 17,
+ 'MEMORY_INIT': 18,
+ 'GAME_IDENTIFIER': 19,
+ 'MOVIE_LICENSE': 20,
+ 'MOVIE_FILE': 21,
+ 'PORT_CONTROLLER': 240,
+ // NES Specific Keys
+ 'NES_LATCH_FILTER': 257,
+ 'NES_CLOCK_FILTER': 258,
+ 'NES_OVERREAD': 259,
+ 'NES_GAME_GENIE_CODE': 260,
+ // SNES Specific Keys
+ 'SNES_CLOCK_FILTER': 514,
+ 'SNES_OVERREAD': 515,
+ 'SNES_GAME_GENIE_CODE': 516,
+ 'SNES_LATCH_TRAIN': 517,
+ // Genesis Specific Keys
+ 'GENESIS_GAME_GENIE_CODE': 2052,
+ // Input Frame/Timing Keys
+ 'INPUT_CHUNK': 65025,
+ 'INPUT_MOMENT': 65026,
+ 'TRANSITION': 65027,
+ 'LAG_FRAME_CHUNK': 65028,
+ 'MOVIE_TRANSITION': 65029,
+ // Extraneous Keys
+ 'COMMENT': 65281,
+ 'EXPERIMENTAL': 65534,
+ 'UNSPECIFIED': 65535,
+} as const;
+export function packetName(key: number) {
+ for (const name of Object.keys(PACKET_TYPES)) {
+ if (PACKET_TYPES[name as keyof typeof PACKET_TYPES] === key) return name;
+ }
+ return 'Unknown Packet ' + key.toString(16).toUpperCase();
+}
diff --git a/src/file.ts b/src/file.ts
new file mode 100644
index 0000000..82194c3
--- /dev/null
+++ b/src/file.ts
@@ -0,0 +1,90 @@
+import { packetName, PACKET_TYPES } from "./constants";
+import { parse } from "./packets";
+import { DumpLastModifiedPacket, PortControllerPacket } from "./packets/general";
+import { InputChunkPacket } from "./packets/input";
+import { TASDPacket } from "./packets/utils";
+import { readUint16, readUint8, readUintN, writeUint16, writeUint8, concatUint8Array } from "./utils";
+
+export class TASD {
+ static MAGIC = new Uint8Array([0x54, 0x41, 0x53, 0x44]);
+ public version: number;
+ public g_keylen: number;
+ public packets: TASDPacket[];
+ private _isDirty: boolean = false;
+ constructor(_version: number, _keylen: number) {
+ this.version = _version;
+ this.g_keylen = _keylen;
+ this.packets = [];
+ }
+ get dirty() {
+ return this._isDirty;
+ }
+ markDirty() {
+ this._isDirty = true;
+ }
+ toBuffer() {
+ const header = new Uint8Array(7);
+ header.set(TASD.MAGIC, 0);
+ writeUint16(this.version, header, 4);
+ writeUint8(this.g_keylen, header, 6);
+ const packets = [ header ];
+ if (this._isDirty) {
+ this.packets = this.packets.filter((packet) => packet.key === PACKET_TYPES.DUMP_LAST_MODIFIED);
+ this.packets.push(new DumpLastModifiedPacket(BigInt(Date.now())));
+ }
+ for (const packet of this.packets) {
+ packets.push(packet.toBuffer(this.g_keylen));
+ }
+ return concatUint8Array(packets);
+ }
+ pprint() {
+ console.log('VERSION', this.version);
+ console.log('G_KEYLEN', this.g_keylen);
+ for (const packet of this.packets) {
+ console.log(packet.toString());
+ }
+ }
+ compactInputs() {
+ if (this.packets.some(packet => packet.key === PACKET_TYPES.INPUT_MOMENT)) return;
+ const otherPackets = this.packets.filter(packet => packet.key !== PACKET_TYPES.INPUT_CHUNK);
+ const inputPackets = this.packets.filter(packet => packet.key === PACKET_TYPES.INPUT_CHUNK);
+ const ports: Uint8Array[][] = [];
+ for (const packet of otherPackets.filter(packet => packet.key === PACKET_TYPES.PORT_CONTROLLER)) {
+ ports[(packet as PortControllerPacket).port] = [];
+ }
+ for (const packet of inputPackets as InputChunkPacket[]) {
+ ports[packet.port].push(packet.inputs);
+ }
+ for (let port = 1; port < 256; port++) {
+ if (ports[port]) {
+ otherPackets.push(new InputChunkPacket(port, concatUint8Array(ports[port])));
+ }
+ }
+ this.packets = otherPackets;
+ this.markDirty();
+ }
+ static parseFile(buffer: Uint8Array) {
+ if (buffer.length < 7) throw new Error('Input Too Short');
+ for (let i = 0; i < TASD.MAGIC.length; i++) {
+ if (buffer[i] !== TASD.MAGIC[i]) throw new Error('Invalid Magic Bytes');
+ }
+ const version = readUint16(buffer, 4);
+ if (version !== 1) throw new Error('Unsupported Version');
+ const g_keylen = readUint8(buffer, 6);
+ if (g_keylen !== 2) throw new Error('G_KEYLEN not equal to 2, unsupported');
+ const file = new TASD(version, g_keylen);
+ let index = 7;
+ while (index < buffer.length) {
+ const p_key = readUintN(buffer, index, g_keylen);
+ index += g_keylen;
+ const p_pexp = readUint8(buffer, index);
+ index += 1;
+ const p_plen = readUintN(buffer, index, p_pexp);
+ index += p_pexp;
+ const p_payload = buffer.subarray(index, index+p_plen);
+ index += p_plen;
+ file.packets.push(parse(p_key, p_payload));
+ }
+ return file;
+ }
+}
diff --git a/src/lib.ts b/src/lib.ts
index ca3c518..fdb8cde 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -1,219 +1,3 @@
-export class TASD {
- static MAGIC = new Uint8Array([0x54, 0x41, 0x53, 0x44]);
- public version: number;
- public g_keylen: number;
- public packets: TASDPacket[];
- constructor(_version: number, _keylen: number) {
- this.version = _version;
- this.g_keylen = _keylen;
- this.packets = [];
- }
- static parseFile(buffer: Uint8Array) {
- if (buffer.length < 7) throw new Error('Input Too Short');
- for (let i = 0; i < TASD.MAGIC.length; i++) {
- if (buffer[i] !== TASD.MAGIC[i]) throw new Error('Invalid Magic Bytes');
- }
- const version = readUint16(buffer, 4);
- if (version !== 1) throw new Error('Unsupported Version');
- const g_keylen = readUint8(buffer, 6);
- if (g_keylen !== 2) throw new Error('G_KEYLEN not equal to 2, unsupported');
- const file = new TASD(version, g_keylen);
- let index = 7;
- while (index < buffer.length) {
- const p_key = readUintN(buffer, index, g_keylen);
- index += g_keylen;
- const p_pexp = readUint8(buffer, index);
- index += 1;
- const p_plen = readUintN(buffer, index, p_pexp);
- index += p_pexp;
- const p_payload = buffer.subarray(index, index+p_plen);
- index += p_plen;
- file.packets.push({ type: p_key, payload: p_payload });
- }
- return file;
- }
- toBuffer() {
- let size = 7; // Header
- for (const packet of this.packets) {
- size += this.g_keylen; // Packet Key
- size += 1; // Packet PEXP
- size += minUInt(packet.payload.length); // Packet PLen
- size += packet.payload.length;
- }
- const buffer = new Uint8Array(size);
- buffer.set(TASD.MAGIC, 0);
- writeUint16(this.version, buffer, 4);
- writeUint8(this.g_keylen, buffer, 6);
- let index = 7;
- for (const packet of this.packets) {
- writeUintN(packet.type, buffer, index, this.g_keylen);
- index += this.g_keylen;
- const plen = packet.payload.length;
- const pexp = minUInt(plen);
- writeUint8(pexp, buffer, index);
- index += 1;
- writeUintN(plen, buffer, index, pexp);
- index += pexp;
- buffer.set(packet.payload, index);
- index += plen;
- }
- return buffer;
- }
- pprint() {
- console.log('VERSION', this.version);
- console.log('G_KEYLEN', this.g_keylen);
- for (const packet of this.packets) {
- console.log('PACKET', packetName(packet.type), packet.payload.length, 'Bytes');
- }
- }
- compactInputs() {
- if (this.packets.some(packet => packet.type === PACKET_TYPES.INPUT_MOMENT)) return;
- const otherPackets = this.packets.filter(packet => packet.type !== PACKET_TYPES.INPUT_CHUNK);
- const inputPackets = this.packets.filter(packet => packet.type === PACKET_TYPES.INPUT_CHUNK);
- const ports: Uint8Array[][] = [];
- for (const packet of otherPackets.filter(packet => packet.type === PACKET_TYPES.PORT_CONTROLLER)) {
- ports[packet.payload[0]] = [];
- }
- for (const packet of inputPackets) {
- ports[packet.payload[0]].push(packet.payload.subarray(1));
- }
- for (let port = 1; port < 256; port++) {
- if (ports[port]) {
- const portN = new Uint8Array([port]);
- otherPackets.push({ type: PACKET_TYPES.INPUT_CHUNK, payload: concatUint8Array([portN, ...ports[port]]) });
- }
- }
- this.packets = otherPackets;
- }
-}
-
-export type TASDPacket = {
- type: number;
- payload: Uint8Array;
-}
-
-export const PACKET_TYPES = {
- // General Keys
- 'CONSOLE_TYPE': 0x00_01,
- 'CONSOLE_REGION': 0x00_02,
- 'GAME_TITLE': 0x00_03,
- 'ROM_NAME': 0x00_04,
- 'ATTRIBUTION': 0x00_05,
- 'CATEGORY': 0x00_06,
- 'EMULATOR_NAME': 0x00_07,
- 'EMULATOR_VERSION': 0x00_08,
- 'EMULATOR_CORE': 0x00_09,
- 'TAS_LAST_MODIFIED': 0x00_0A,
- 'DUMP_CREATED': 0x00_0B,
- 'DUMP_LAST_MODIFIED': 0x00_0C,
- 'TOTAL_FRAMES': 0x00_0D,
- 'RERECORDS': 0x00_0E,
- 'SOURCE_LINK': 0x00_0F,
- 'BLANK_FRAMES': 0x00_10,
- 'VERIFIED': 0x00_11,
- 'MEMORY_INIT': 0x00_12,
- 'GAME_IDENTIFIER': 0x00_13,
- 'MOVIE_LICENSE': 0x00_14,
- 'MOVIE_FILE': 0x00_15,
- 'PORT_CONTROLLER': 0x00_F0,
- // NES Specific Keys
- 'NES_LATCH_FILTER': 0x01_01,
- 'NES_CLOCK_FILTER': 0x01_02,
- 'NES_OVERREAD': 0x01_03,
- 'NES_GAME_GENIE_CODE': 0x01_04,
- // SNES Specific Keys
- 'SNES_CLOCK_FILTER': 0x02_02,
- 'SNES_OVERREAD': 0x02_03,
- 'SNES_GAME_GENIE_CODE': 0x02_04,
- 'SNES_LATCH_TRAIN': 0x02_05,
- // Genesis Specific Keys
- 'GENESIS_GAME_GENIE_CODE': 0x08_04,
- // Input Frame/Timing Keys
- 'INPUT_CHUNK': 0xFE_01,
- 'INPUT_MOMENT': 0xFE_02,
- 'TRANSITION': 0xFE_03,
- 'LAG_FRAME_CHUNK': 0xFE_04,
- 'MOVIE_TRANSITION': 0xFE_05,
- // Extraneous Keys
- 'COMMENT': 0xFF_01,
- 'EXPERIMENTAL': 0xFF_FE,
- 'UNSPECIFIED': 0xFF_FF,
-} as const;
-export function packetName(key: number) {
- for (const name of Object.keys(PACKET_TYPES)) {
- if (PACKET_TYPES[name as keyof typeof PACKET_TYPES] === key) return name;
- }
- return 'Unknown Packet ' + key.toString(16).toUpperCase();
-}
-
-export function readUint8(buffer: Uint8Array, index: number) {
- return buffer[index];
-}
-export function readUint16(buffer: Uint8Array, index: number) {
- return (buffer[index] << 8) + buffer[index+1];
-}
-export function readUint24(buffer: Uint8Array, index: number) {
- return (buffer[index] << 16) + (buffer[index+1] << 8) + buffer[index+2];
-}
-export function readUint32(buffer: Uint8Array, index: number) {
- return (buffer[index] << 24) + (buffer[index+1] << 16) + (buffer[index+2] << 8) + buffer[index+3];
-}
-export function readUintN(buffer: Uint8Array, index: number, n: number) {
- switch (n) {
- case 1: return readUint8(buffer, index);
- case 2: return readUint16(buffer, index);
- case 3: return readUint24(buffer, index);
- case 4: return readUint32(buffer, index);
- default: throw new Error('Unsupported UInt size');
- }
-}
-
-export function writeUint8(uint: number, buffer: Uint8Array, index: number) {
- buffer[index] = uint & 0xFF;
-}
-export function writeUint16(uint: number, buffer: Uint8Array, index: number) {
- buffer[index] = (uint & 0xFF00) >> 8;
- buffer[index+1] = uint & 0xFF;
-}
-export function writeUint24(uint: number, buffer: Uint8Array, index: number) {
- buffer[index] = (uint & 0xFF0000) >> 16;
- buffer[index+1] = (uint & 0xFF00) >> 8;
- buffer[index+2] = uint & 0xFF;
-}
-export function writeUint32(uint: number, buffer: Uint8Array, index: number) {
- buffer[index] = (uint & 0xFF000000) >> 24;
- buffer[index+1] = (uint & 0xFF0000) >> 16;
- buffer[index+2] = (uint & 0xFF00) >> 8;
- buffer[index+3] = uint & 0xFF;
-}
-export function writeUintN(uint: number, buffer: Uint8Array, index: number, size: number) {
- switch (size) {
- case 1: return writeUint8(uint, buffer, index);
- case 2: return writeUint16(uint, buffer, index);
- case 3: return writeUint24(uint, buffer, index);
- case 4: return writeUint32(uint, buffer, index);
- default: throw new Error('Unsupported UInt size');
- }
-}
-
-export function minUInt(uint: number) {
- if (uint < 256) return 1;
- if (uint < 65536) return 2;
- if (uint < 16777216) return 3;
- if (uint < 4294967296) return 4;
- throw new Error('Unsupported UInt size');
-}
-
-export function concatUint8Array(arrays: Uint8Array[]): Uint8Array {
- let size = 0;
- for (const array of arrays) {
- size += array.length;
- }
- const buffer = new Uint8Array(size);
- let index = 0;
- for (const array of arrays) {
- buffer.set(array, index);
- index += array.length;
- }
- return buffer;
-}
+export { TASD } from './file';
+export * as Packet from './packets';
+export * as utils from './utils'; \ No newline at end of file
diff --git a/src/packets/general.ts b/src/packets/general.ts
new file mode 100644
index 0000000..9b80818
--- /dev/null
+++ b/src/packets/general.ts
@@ -0,0 +1,398 @@
+import { PACKET_TYPES } from "../constants";
+import { TASD } from "../file";
+import { encodeString, readString, readUint16, readUint32, readUint64, readUint8, writeString, writeUint16, writeUint32, writeUint64, writeUint8 } from "../utils";
+import { TASDPacket, buildBuffer } from "./utils";
+
+export class ConsoleTypePacket implements TASDPacket {
+ constructor(public type: number, public name: string) {}
+ get key() {
+ return PACKET_TYPES.CONSOLE_TYPE;
+ }
+ get size() {
+ return this.name.length + 1;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new ConsoleTypePacket(buffer[0], readString(buffer, 1, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(this.name.length + 1);
+ writeUint8(this.type, payload, 0);
+ writeString(this.name, payload, 1);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `ConsoleType ${this.type}, ${this.name}`;
+ }
+}
+
+export class ConsoleRegionPacket implements TASDPacket {
+ constructor(public region: number) {}
+ get key() {
+ return PACKET_TYPES.CONSOLE_REGION;
+ }
+ get size() {
+ return 1;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new ConsoleRegionPacket(buffer[0]);
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(1);
+ writeUint8(this.region, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `ConsoleRegion ${this.region}`;
+ }
+}
+
+export class GameTitlePacket implements TASDPacket {
+ constructor(public title: string) {}
+ get key() {
+ return PACKET_TYPES.GAME_TITLE;
+ }
+ get size() {
+ return this.title.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new GameTitlePacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.title);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `GameTitle ${this.title}`;
+ }
+}
+
+export class ROMNamePacket implements TASDPacket {
+ constructor(public name: string) {}
+ get key() {
+ return PACKET_TYPES.ROM_NAME;
+ }
+ get size() {
+ return this.name.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new ROMNamePacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.name);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `RomName ${this.name}`;
+ }
+}
+
+export class AttributionPacket implements TASDPacket {
+ constructor(public type: number, public name: string) {}
+ get key() {
+ return PACKET_TYPES.ATTRIBUTION;
+ }
+ get size() {
+ return this.name.length + 1;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new AttributionPacket(buffer[0], readString(buffer, 1, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(this.name.length + 1);
+ writeUint8(this.type, payload, 0);
+ writeString(this.name, payload, 1);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `Attribution ${this.type}, ${this.name}`;
+ }
+}
+
+export class CategoryPacket implements TASDPacket {
+ constructor(public name: string) {}
+ get key() {
+ return PACKET_TYPES.CATEGORY;
+ }
+ get size() {
+ return this.name.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new CategoryPacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.name);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `Category ${this.name}`;
+ }
+}
+
+export class EmulatorNamePacket implements TASDPacket {
+ constructor(public name: string) {}
+ get key() {
+ return PACKET_TYPES.EMULATOR_NAME;
+ }
+ get size() {
+ return this.name.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new EmulatorNamePacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.name);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `EmulatorName ${this.name}`;
+ }
+}
+
+export class EmulatorVersionPacket implements TASDPacket {
+ constructor(public version: string) {}
+ get key() {
+ return PACKET_TYPES.EMULATOR_VERSION;
+ }
+ get size() {
+ return this.version.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new EmulatorVersionPacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.version);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `EmulatorVersion ${this.version}`;
+ }
+}
+
+export class EmulatorCorePacket implements TASDPacket {
+ constructor(public core: string) {}
+ get key() {
+ return PACKET_TYPES.EMULATOR_CORE;
+ }
+ get size() {
+ return this.core.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new EmulatorCorePacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.core);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `EmulatorCore ${this.core}`;
+ }
+}
+
+export class TASLastModifedPacket implements TASDPacket {
+ constructor(public timestamp: bigint) {}
+ get key() {
+ return PACKET_TYPES.TAS_LAST_MODIFIED;
+ }
+ get size() {
+ return 8;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new TASLastModifedPacket(readUint64(buffer, 0));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(8);
+ writeUint64(this.timestamp, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `TASLastModified ${new Date(Number(this.timestamp * 1000n)).toUTCString()}`;
+ }
+}
+
+export class DumpCreatedPacket implements TASDPacket {
+ constructor(public timestamp: bigint) {}
+ get key() {
+ return PACKET_TYPES.DUMP_CREATED;
+ }
+ get size() {
+ return 8;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new DumpCreatedPacket(readUint64(buffer, 0));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(8);
+ writeUint64(this.timestamp, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `DumpCreated ${new Date(Number(this.timestamp * 1000n)).toUTCString()}`;
+ }
+}
+
+export class DumpLastModifiedPacket implements TASDPacket {
+ constructor(public timestamp: bigint) {}
+ get key() {
+ return PACKET_TYPES.DUMP_LAST_MODIFIED;
+ }
+ get size() {
+ return 8;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new DumpLastModifiedPacket(readUint64(buffer, 0));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(8);
+ writeUint64(this.timestamp, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `DumpLastModified ${new Date(Number(this.timestamp * 1000n)).toUTCString()}`;
+ }
+}
+
+export class TotalFramesPacket implements TASDPacket {
+ constructor(public frames: number) {}
+ get key() {
+ return PACKET_TYPES.TOTAL_FRAMES;
+ }
+ get size() {
+ return 4;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new TotalFramesPacket(readUint32(buffer, 0));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(4);
+ writeUint32(this.frames, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `TotalFrames ${this.frames}`;
+ }
+}
+
+export class RerecordsPacket implements TASDPacket {
+ constructor(public rerecords: number) {}
+ get key() {
+ return PACKET_TYPES.RERECORDS;
+ }
+ get size() {
+ return 4;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new RerecordsPacket(readUint32(buffer, 0));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(4);
+ writeUint32(this.rerecords, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `Rerecords ${this.rerecords}`;
+ }
+}
+
+export class SourceLinkPacket implements TASDPacket {
+ constructor(public link: string) {}
+ get key() {
+ return PACKET_TYPES.SOURCE_LINK;
+ }
+ get size() {
+ return this.link.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new SourceLinkPacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.link);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `SourceLink ${this.link}`;
+ }
+}
+
+export class BlankFramesPacket implements TASDPacket {
+ constructor(public frames: number) {}
+ get key() {
+ return PACKET_TYPES.BLANK_FRAMES;
+ }
+ get size() {
+ return 2;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new BlankFramesPacket(readUint16(buffer, 0));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(2);
+ writeUint16(this.frames, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `BlankFrames ${this.frames}`;
+ }
+}
+
+export class VerifiedPacket implements TASDPacket {
+ constructor(public verified: boolean) {}
+ get key() {
+ return PACKET_TYPES.VERIFIED;
+ }
+ get size() {
+ return 1;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new VerifiedPacket(readUint8(buffer, 0) !== 0);
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(2);
+ writeUint8(this.verified ? 1 : 0, payload, 0);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `Verified ${this.verified}`;
+ }
+}
+
+export class MovieLicensePacket implements TASDPacket {
+ constructor(public link: string) {}
+ get key() {
+ return PACKET_TYPES.MOVIE_LICENSE;
+ }
+ get size() {
+ return this.link.length;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new MovieLicensePacket(readString(buffer, 0, buffer.length));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = encodeString(this.link);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `SourceLink ${this.link}`;
+ }
+}
+
+export class PortControllerPacket implements TASDPacket {
+ constructor(public port: number, public type: number) {}
+ get key() {
+ return PACKET_TYPES.PORT_CONTROLLER;
+ }
+ get size() {
+ return 3;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new PortControllerPacket(buffer[0], readUint16(buffer, 1));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(3);
+ writeUint8(this.port, payload, 0);
+ writeUint16(this.type, payload, 1);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `PortController ${this.port}, 0x${this.type.toString(16).padStart(4, '0')}`;
+ }
+} \ No newline at end of file
diff --git a/src/packets/index.ts b/src/packets/index.ts
new file mode 100644
index 0000000..0bf2195
--- /dev/null
+++ b/src/packets/index.ts
@@ -0,0 +1,58 @@
+import { PACKET_TYPES } from "../constants";
+import { AttributionPacket, BlankFramesPacket, CategoryPacket, ConsoleRegionPacket, ConsoleTypePacket, DumpCreatedPacket, DumpLastModifiedPacket, EmulatorCorePacket, EmulatorNamePacket, EmulatorVersionPacket, GameTitlePacket, MovieLicensePacket, PortControllerPacket, ROMNamePacket, RerecordsPacket, SourceLinkPacket, TASLastModifedPacket, TotalFramesPacket, VerifiedPacket } from "./general";
+import { InputChunkPacket } from "./input";
+import { TASDPacket, UnknownPacket } from "./utils";
+export * as General from './general';
+export * as Input from './input';
+
+export function parse(type: number, buffer: Uint8Array): TASDPacket {
+ switch (type) {
+ // General
+ case PACKET_TYPES.CONSOLE_TYPE: return ConsoleTypePacket.fromBuffer(buffer);
+ case PACKET_TYPES.CONSOLE_REGION: return ConsoleRegionPacket.fromBuffer(buffer);
+ case PACKET_TYPES.GAME_TITLE: return GameTitlePacket.fromBuffer(buffer);
+ case PACKET_TYPES.ROM_NAME: return ROMNamePacket.fromBuffer(buffer);
+ case PACKET_TYPES.ATTRIBUTION: return AttributionPacket.fromBuffer(buffer);
+ case PACKET_TYPES.CATEGORY: return CategoryPacket.fromBuffer(buffer);
+ case PACKET_TYPES.EMULATOR_NAME: return EmulatorNamePacket.fromBuffer(buffer);
+ case PACKET_TYPES.EMULATOR_VERSION: return EmulatorVersionPacket.fromBuffer(buffer);
+ case PACKET_TYPES.EMULATOR_CORE: return EmulatorCorePacket.fromBuffer(buffer);
+ case PACKET_TYPES.TAS_LAST_MODIFIED: return TASLastModifedPacket.fromBuffer(buffer);
+ case PACKET_TYPES.DUMP_CREATED: return DumpCreatedPacket.fromBuffer(buffer);
+ case PACKET_TYPES.DUMP_LAST_MODIFIED: return DumpLastModifiedPacket.fromBuffer(buffer);
+ case PACKET_TYPES.TOTAL_FRAMES: return TotalFramesPacket.fromBuffer(buffer);
+ case PACKET_TYPES.RERECORDS: return RerecordsPacket.fromBuffer(buffer);
+ case PACKET_TYPES.SOURCE_LINK: return SourceLinkPacket.fromBuffer(buffer);
+ case PACKET_TYPES.BLANK_FRAMES: return BlankFramesPacket.fromBuffer(buffer);
+ case PACKET_TYPES.VERIFIED: return VerifiedPacket.fromBuffer(buffer);
+ // case PACKET_TYPES.MEMORY_INIT: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.GAME_IDENTIFIER: return XXXXX.fromBuffer(buffer);
+ case PACKET_TYPES.MOVIE_LICENSE: return MovieLicensePacket.fromBuffer(buffer);
+ // case PACKET_TYPES.MOVIE_FILE: return XXXXX.fromBuffer(buffer);
+ case PACKET_TYPES.PORT_CONTROLLER: return PortControllerPacket.fromBuffer(buffer);
+ // NES
+ // case PACKET_TYPES.NES_LATCH_FILTER: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.NES_CLOCK_FILTER: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.NES_OVERREAD: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.NES_GAME_GENIE_CODE: return XXXXX.fromBuffer(buffer);
+ // SNES
+ // case PACKET_TYPES.SNES_CLOCK_FILTER: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.SNES_OVERREAD: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.SNES_GAME_GENIE_CODE: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.SNES_LATCH_TRAIN: return XXXXX.fromBuffer(buffer);
+ // Genesis
+ // case PACKET_TYPES.GENESIS_GAME_GENIE_CODE: return XXXXX.fromBuffer(buffer);
+ // Input
+ case PACKET_TYPES.INPUT_CHUNK: return InputChunkPacket.fromBuffer(buffer);
+ // case PACKET_TYPES.INPUT_MOMENT: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.TRANSITION: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.LAG_FRAME_CHUNK: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.MOVIE_TRANSITION: return XXXXX.fromBuffer(buffer);
+ // Extra
+ // case PACKET_TYPES.COMMENT: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.EXPERIMENTAL: return XXXXX.fromBuffer(buffer);
+ // case PACKET_TYPES.UNSPECIFIED: return XXXXX.fromBuffer(buffer);
+ // Unknown
+ default: return new UnknownPacket(type, buffer);
+ }
+} \ No newline at end of file
diff --git a/src/packets/input.ts b/src/packets/input.ts
new file mode 100644
index 0000000..b084ebe
--- /dev/null
+++ b/src/packets/input.ts
@@ -0,0 +1,24 @@
+import { PACKET_TYPES } from "../constants";
+import { TASDPacket, buildBuffer } from "./utils";
+
+export class InputChunkPacket implements TASDPacket {
+ constructor(public port: number, public inputs: Uint8Array) {}
+ get key() {
+ return PACKET_TYPES.INPUT_CHUNK;
+ }
+ get size() {
+ return this.inputs.length + 1;
+ }
+ static fromBuffer(buffer: Uint8Array) {
+ return new InputChunkPacket(buffer[0], buffer.subarray(1));
+ }
+ toBuffer(g_keylen: number): Uint8Array {
+ const payload = new Uint8Array(this.inputs.length + 1);
+ payload[0] = this.port;
+ payload.set(this.inputs, 1);
+ return buildBuffer(g_keylen, this.key, payload);
+ }
+ toString() {
+ return `InputChunk ${this.port}, ${this.size}`;
+ }
+} \ No newline at end of file
diff --git a/src/packets/utils.ts b/src/packets/utils.ts
new file mode 100644
index 0000000..3a5befc
--- /dev/null
+++ b/src/packets/utils.ts
@@ -0,0 +1,37 @@
+import { packetName } from "../constants";
+import { minUInt, writeUintN, writeUint8 } from "../utils";
+
+export interface TASDPacket {
+ key: number;
+ size: number;
+ toBuffer(g_keylen: number): Uint8Array;
+ toString(): string;
+}
+
+export function buildBuffer(g_keylen: number, key: number, payload: Uint8Array) {
+ const plen = payload.length;
+ const pexp = minUInt(plen);
+ const buffer = new Uint8Array(g_keylen + 1 + pexp + plen);
+ let index = 0;
+ writeUintN(key, buffer, index, g_keylen);
+ index += g_keylen;
+ writeUint8(pexp, buffer, index);
+ index += 1;
+ writeUintN(plen, buffer, index, pexp);
+ index += pexp;
+ buffer.set(payload, index);
+ return buffer;
+}
+
+export class UnknownPacket implements TASDPacket {
+ constructor(public key: number, public payload: Uint8Array) {}
+ get size() {
+ return this.payload.length;
+ }
+ toBuffer(g_keylen: number) {
+ return buildBuffer(g_keylen, this.key, this.payload);
+ }
+ toString() {
+ return `UnknownPacket ${packetName(this.key)}, ${this.size}`;
+ }
+} \ No newline at end of file
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..f47f584
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,95 @@
+
+export function readUint8(buffer: Uint8Array, index: number) {
+ return buffer[index];
+}
+export function readUint16(buffer: Uint8Array, index: number) {
+ return (buffer[index] << 8) + buffer[index + 1];
+}
+export function readUint24(buffer: Uint8Array, index: number) {
+ return (buffer[index] << 16) + (buffer[index + 1] << 8) + buffer[index + 2];
+}
+export function readUint32(buffer: Uint8Array, index: number) {
+ return ((buffer[index] << 24) + (buffer[index + 1] << 16) + (buffer[index + 2] << 8) + buffer[index + 3]) >>> 0;
+}
+export function readUint64(buffer: Uint8Array, index: number) {
+ const left = readUint32(buffer, index);
+ const right = readUint32(buffer, index+4);
+ return (BigInt(left) << 32n) + BigInt(right);
+}
+export function readUintN(buffer: Uint8Array, index: number, n: number) {
+ switch (n) {
+ case 1: return readUint8(buffer, index);
+ case 2: return readUint16(buffer, index);
+ case 3: return readUint24(buffer, index);
+ case 4: return readUint32(buffer, index);
+ default: throw new Error('Unsupported UInt size');
+ }
+}
+
+export function writeUint8(uint: number, buffer: Uint8Array, index: number) {
+ buffer[index] = uint & 255;
+}
+export function writeUint16(uint: number, buffer: Uint8Array, index: number) {
+ buffer[index] = (uint & 65280) >> 8;
+ buffer[index + 1] = uint & 255;
+}
+export function writeUint24(uint: number, buffer: Uint8Array, index: number) {
+ buffer[index] = (uint & 16711680) >> 16;
+ buffer[index + 1] = (uint & 65280) >> 8;
+ buffer[index + 2] = uint & 255;
+}
+export function writeUint32(uint: number, buffer: Uint8Array, index: number) {
+ buffer[index] = (uint & 4278190080) >> 24;
+ buffer[index + 1] = (uint & 16711680) >> 16;
+ buffer[index + 2] = (uint & 65280) >> 8;
+ buffer[index + 3] = uint & 255;
+}
+export function writeUint64(uint: bigint, buffer: Uint8Array, index: number) {
+ const left = (uint & 0xffffffff00000000n) >> 32n;
+ const right = uint & 0xffffffffn;
+ writeUint32(Number(left), buffer, index);
+ writeUint32(Number(right), buffer, index+4);
+}
+export function writeUintN(uint: number, buffer: Uint8Array, index: number, size: number) {
+ switch (size) {
+ case 1: return writeUint8(uint, buffer, index);
+ case 2: return writeUint16(uint, buffer, index);
+ case 3: return writeUint24(uint, buffer, index);
+ case 4: return writeUint32(uint, buffer, index);
+ default: throw new Error('Unsupported UInt size');
+ }
+}
+
+export function minUInt(uint: number) {
+ if (uint < 256) return 1;
+ if (uint < 65536) return 2;
+ if (uint < 16777216) return 3;
+ if (uint < 4294967296) return 4;
+ throw new Error('Unsupported UInt size');
+}
+
+export function concatUint8Array(arrays: Uint8Array[]): Uint8Array {
+ let size = 0;
+ for (const array of arrays) {
+ size += array.length;
+ }
+ const buffer = new Uint8Array(size);
+ let index = 0;
+ for (const array of arrays) {
+ buffer.set(array, index);
+ index += array.length;
+ }
+ return buffer;
+}
+
+const decoder = new TextDecoder('utf8');
+const encoder = new TextEncoder();
+export function readString(buffer: Uint8Array, index: number, length: number) {
+ return decoder.decode(buffer.subarray(index, index+length));
+}
+export function writeString(str: string, buffer: Uint8Array, index: number) {
+ buffer.set(encoder.encode(str), index);
+}
+export function encodeString(str: string) {
+ return encoder.encode(str);
+} \ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index ef2f1be..215a277 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,11 +5,11 @@
"sourceMap": true,
"outDir": "dist",
"declaration": true,
- "target": "es6",
+ "target": "es2020",
"strict": true
},
"lib": [
- "es2015"
+ "es2020"
],
"include": [
"src/**/*"