/** Bounds-checked access to the user's cartridge. No CPU or hardware state. */ export class RomReader { constructor(readonly bytes: Uint8Array) {} byte(offset: number): number { this.check(offset, 1); return this.bytes[offset]; } word(offset: number): number { return this.byte(offset) | (this.byte(offset + 1) << 8); } slice(offset: number, length: number): Uint8Array { this.check(offset, length); return this.bytes.slice(offset, offset + length); } address(bank: number, address: number): number { if ( !Number.isInteger(bank) || bank < 0 || bank >= this.bytes.length / 0x4000 || !Number.isInteger(address) || address < 0 || address >= 0x8000 ) { throw new Error("Invalid ROM bank or address."); } const offset = address < 0x4000 ? address : bank * 0x4000 + address - 0x4000; this.check(offset, 1); return offset; } near(pointer: number, bank: number): number { return this.address(bank, this.word(pointer)); } far(pointer: number): number { return this.near(pointer + 1, this.byte(pointer)); } bank(offset: number): number { this.check(offset, 1); return Math.floor(offset / 0x4000); } private check(offset: number, length: number) { if ( !Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0 || offset + length > this.bytes.length ) { throw new Error( "Cartridge data is truncated or contains an invalid pointer.", ); } } }