From f2aab487c22f4919d48896d33cd0f09ba0928395 Mon Sep 17 00:00:00 2001 From: TheMas3212 Date: Sat, 23 Sep 2023 12:21:43 +1000 Subject: Add additional Packet Types --- README.md | 3 + package.json | 3 +- src/constants.ts | 54 +++++++ src/file.ts | 90 +++++++++++ src/lib.ts | 222 +-------------------------- src/packets/general.ts | 398 +++++++++++++++++++++++++++++++++++++++++++++++++ src/packets/index.ts | 58 +++++++ src/packets/input.ts | 24 +++ src/packets/utils.ts | 37 +++++ src/utils.ts | 95 ++++++++++++ tsconfig.json | 4 +- 11 files changed, 766 insertions(+), 222 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/file.ts create mode 100644 src/packets/general.ts create mode 100644 src/packets/index.ts create mode 100644 src/packets/input.ts create mode 100644 src/packets/utils.ts create mode 100644 src/utils.ts 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/**/*" -- cgit v1.2.3