/
cartridge-parser.ts
117 lines (96 loc) · 3.34 KB
/
cartridge-parser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import {MirroringMode} from './mirroring-mode';
interface CartridgeHeader {
prgSize16K: number;
chrSize8K: number;
mapper: number;
haveTrainer: boolean;
mirroring: number;
}
interface BaseCartridgeData<T> {
header: CartridgeHeader;
prgRom: T;
/* chr can be rom or ram, so chrData is a good name */
chrData: T;
}
type CartridgeData = BaseCartridgeData<Uint8Array>;
export type SerializedCartridgeData = BaseCartridgeData<number[]>;
export const enum CartridgeConstants {
HEADER_OFFSET = 16,
TRAINER_OFFSET = 512,
PRG_BANK_SIZE = 16384,
CHR_BANK_SIZE = 8192,
// "NES" followed by MS-DOS end-of-file
NES_FILE_FORMAT = 'NES\u001a'
}
function getPrgRomOffset(header: CartridgeHeader): number {
return header.haveTrainer ?
CartridgeConstants.TRAINER_OFFSET + CartridgeConstants.HEADER_OFFSET :
CartridgeConstants.HEADER_OFFSET;
}
function getChrRomOffset(header: CartridgeHeader): number {
return getPrgRomOffset(header) + header.prgSize16K * CartridgeConstants.PRG_BANK_SIZE;
}
function parseHeader(data: Uint8Array): CartridgeHeader {
const fileFormat = [data[0], data[1], data[2], data[3]].map((n) => String.fromCharCode(n)).join('');
if (fileFormat !== CartridgeConstants.NES_FILE_FORMAT) {
throw new Error('Cartridge does not match iNES file format');
}
const prgSize16K = data[4];
const chrSize8K = data[5];
const flags6 = data[6];
const flags7 = data[7];
const haveTrainer = Boolean(flags6 & 0b100);
const loMapper = flags6 & 0b11110000;
const hiMapper = flags7 & 0b11110000;
const mapper = hiMapper | (loMapper >> 4);
const mirroring = (flags6 & 0x1) === 0 ?
MirroringMode.HORIZONTAL : MirroringMode.VERTICAL;
return {
prgSize16K,
chrSize8K,
mapper,
haveTrainer,
mirroring
};
}
function parsePrgRom(data: Uint8Array, header: CartridgeHeader): Uint8Array {
const offset = getPrgRomOffset(header);
return data.slice(offset, offset + CartridgeConstants.PRG_BANK_SIZE * header.prgSize16K);
}
function parseChrRom(data: Uint8Array, header: CartridgeHeader): Uint8Array {
// 0 means board uses chr ram, so allocate it
if (header.chrSize8K === 0) {
return new Uint8Array(CartridgeConstants.CHR_BANK_SIZE);
}
const offset = getChrRomOffset(header);
return data.slice(offset, offset + CartridgeConstants.CHR_BANK_SIZE * header.chrSize8K);
}
// https://wiki.nesdev.com/w/index.php/INES
function parseCartridge(buffer: ArrayBuffer): CartridgeData {
const data = new Uint8Array(buffer);
const header = parseHeader(data);
const prgRom = parsePrgRom(data, header);
const chrData = parseChrRom(data, header);
return {header, prgRom, chrData};
}
function serializeCartridgeData(data: CartridgeData): SerializedCartridgeData {
return {
header: {...data.header},
prgRom: Array.from(data.prgRom.values()),
chrData: Array.from(data.chrData.values())
};
}
function deserializeCartridgeData(serialized: SerializedCartridgeData): CartridgeData {
return {
header: {...serialized.header},
prgRom: Uint8Array.from(serialized.prgRom),
chrData: Uint8Array.from(serialized.chrData)
}
}
export {
CartridgeData,
CartridgeHeader,
parseCartridge,
serializeCartridgeData,
deserializeCartridgeData
};