nitro-patcher 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
1
1
  // Based on NitroHelper FNT/FAT/OverlayTable, GPL-3.0.
2
- import { bytes, range } from "./binary";
2
+ import iconv from "iconv-lite";
3
3
 
4
- /** A file in the original ROM, optionally replaced with new bytes. */
4
+ import { BinaryWriter, bytes, range } from "./binary";
5
+
6
+ /** Original IDs/offsets describe the input ROM; newly added files use -1 until serialized and reopened. */
5
7
  export interface NitroFile {
6
8
  name: string;
9
+ nameBytes?: Buffer;
7
10
  id: number;
8
11
  offset: number;
9
12
  size: number;
@@ -12,11 +15,27 @@ export interface NitroFile {
12
15
 
13
16
  export interface NitroFolder {
14
17
  name: string;
18
+ nameBytes?: Buffer;
15
19
  id: number;
16
20
  files: NitroFile[];
17
21
  folders: NitroFolder[];
18
22
  }
19
23
 
24
+ export const encodeName = (name: string): Buffer => {
25
+ const encoded = iconv.encode(name, "shift_jis");
26
+ if (new TextDecoder("shift_jis").decode(encoded) !== name)
27
+ throw new Error(`FNT name cannot be represented in Shift_JIS: ${name}`);
28
+ if (!encoded.length || encoded.length > 127) throw new Error("FNT name must contain 1 to 127 encoded bytes");
29
+ return encoded;
30
+ };
31
+
32
+ export interface FileSystemLayout {
33
+ fnt: Buffer;
34
+ files: NitroFile[];
35
+ ids: Map<NitroFile, number>;
36
+ sourceIds: Map<number, number>;
37
+ }
38
+
20
39
  export interface FatEntry {
21
40
  id: number;
22
41
  offset: number;
@@ -72,7 +91,8 @@ export class FileNameTable {
72
91
  if (!tag) break;
73
92
  const length = tag & 0x7f;
74
93
  if (!length) throw new Error("Empty FNT directory name");
75
- const filename = decoder.decode(range(table, cursor, length, "FNT filename"));
94
+ const nameBytes = Buffer.from(range(table, cursor, length, "FNT filename"));
95
+ const filename = decoder.decode(nameBytes);
76
96
  cursor += length;
77
97
  if (
78
98
  filename.includes("/") ||
@@ -89,13 +109,15 @@ export class FileNameTable {
89
109
  const id = range(table, cursor, 2, "FNT directory id").readUInt16LE();
90
110
  cursor += 2;
91
111
  if (id < 0xf000) throw new Error("Invalid FNT directory id");
92
- folder.folders.push(parse(id & 0xfff, filename, depth + 1));
112
+ const child = parse(id & 0xfff, filename, depth + 1);
113
+ child.nameBytes = nameBytes;
114
+ folder.folders.push(child);
93
115
  } else {
94
116
  const entry = fat.entries[fileId++];
95
117
  if (!entry) throw new Error("FNT file id is outside FAT");
96
118
  if (fileIds.has(entry.id)) throw new Error(`Duplicate FNT file id: ${entry.id}`);
97
119
  fileIds.add(entry.id);
98
- folder.files.push({ name: filename, ...entry });
120
+ folder.files.push({ name: filename, nameBytes, ...entry });
99
121
  }
100
122
  }
101
123
  return folder;
@@ -103,6 +125,67 @@ export class FileNameTable {
103
125
  this.root = { name: "root", id: 0xffff, files: [], folders: [parse(0, "data", 0)] };
104
126
  }
105
127
 
128
+ serialize(fat: FileAllocationTable, overlays: NitroFile[]): FileSystemLayout {
129
+ const directories: { folder: NitroFolder; parent: number; firstId: number }[] = [];
130
+ const named: NitroFile[] = [];
131
+ const sort = <T extends NitroFile | NitroFolder>(items: T[]): T[] =>
132
+ [...items].sort((a, b) => Buffer.compare(a.nameBytes ?? encodeName(a.name), b.nameBytes ?? encodeName(b.name)));
133
+ const visit = (folder: NitroFolder, parent: number): void => {
134
+ const index = directories.length;
135
+ directories.push({ folder, parent, firstId: named.length });
136
+ if (directories.length > 4096) throw new Error("FNT directory count exceeds 4096");
137
+ named.push(...sort(folder.files));
138
+ for (const child of sort(folder.folders)) visit(child, 0xf000 + index);
139
+ };
140
+ visit(this.root.folders[0], 0xf000);
141
+ const namedIds = new Set(named.filter((file) => file.id >= 0).map((file) => file.id));
142
+ const overlayIds = new Set(overlays.map((file) => file.id));
143
+ const unnamed = fat.entries
144
+ .filter((entry) => !namedIds.has(entry.id))
145
+ .map((entry) => ({ ...entry, name: `FAT entry ${entry.id}` }));
146
+ const files: NitroFile[] = [
147
+ ...unnamed.filter((entry) => overlayIds.has(entry.id)),
148
+ ...overlays
149
+ .filter((file) => file.id < 0)
150
+ .sort(
151
+ (a, b) =>
152
+ Number(a.name.startsWith("overlay7_")) - Number(b.name.startsWith("overlay7_")) ||
153
+ Number(a.name.slice(a.name.indexOf("_") + 1, -4)) - Number(b.name.slice(b.name.indexOf("_") + 1, -4)),
154
+ ),
155
+ ...unnamed.filter((entry) => !overlayIds.has(entry.id)),
156
+ ];
157
+ const firstNamed = files.length;
158
+ files.push(...named);
159
+ if (files.length > 0xf000) throw new Error("Filesystem file IDs exceed 0xEFFF");
160
+ const ids = new Map(files.map((file, id) => [file, id]));
161
+ const sourceIds = new Map(files.filter((file) => file.id >= 0).map((file) => [file.id, ids.get(file)!]));
162
+ const directoryIds = new Map(directories.map(({ folder }, index) => [folder, 0xf000 + index]));
163
+ const writer = new BinaryWriter();
164
+ writer.write(Buffer.alloc(directories.length * 8));
165
+ for (const [index, { folder, parent, firstId }] of directories.entries()) {
166
+ const record = Buffer.alloc(8);
167
+ record.writeUInt32LE(writer.position);
168
+ record.writeUInt16LE(firstNamed + firstId, 4);
169
+ record.writeUInt16LE(index === 0 ? directories.length : parent, 6);
170
+ record.copy(writer.buffer, index * 8);
171
+ for (const file of sort(folder.files)) {
172
+ const name = file.nameBytes ?? encodeName(file.name);
173
+ writer.write(Buffer.from([name.length]));
174
+ writer.write(name);
175
+ }
176
+ for (const child of sort(folder.folders)) {
177
+ const name = child.nameBytes ?? encodeName(child.name);
178
+ writer.write(Buffer.from([0x80 | name.length]));
179
+ writer.write(name);
180
+ const id = Buffer.alloc(2);
181
+ id.writeUInt16LE(directoryIds.get(child)!);
182
+ writer.write(id);
183
+ }
184
+ writer.write(Buffer.from([0]));
185
+ }
186
+ return { fnt: writer.toBuffer(), files, ids, sourceIds };
187
+ }
188
+
106
189
  static findFile(id: number, folder: NitroFolder): NitroFile | undefined {
107
190
  for (const file of folder.files) if (file.id === id) return file;
108
191
  for (const subfolder of folder.folders) {
@@ -77,6 +77,8 @@ export class Header {
77
77
  declare ARM9autoload: number;
78
78
  declare ARM7autoload: number;
79
79
  declare ROMsize: number;
80
+ declare ntr_region_end: number;
81
+ declare twl_region_start: number;
80
82
  declare headerSize: number;
81
83
  declare logoCRC16: number;
82
84
  declare headerCRC16: number;
@@ -219,6 +221,8 @@ field("secureDisable", 0x78, 8, "number");
219
221
  field("ROMsize", 0x80, 4, "number");
220
222
  field("headerSize", 0x84, 4, "number");
221
223
  field("reserved2", 0x88, 56);
224
+ field("ntr_region_end", 0x90, 2, "number");
225
+ field("twl_region_start", 0x92, 2, "number");
222
226
  field("logo", 0xc0, 156);
223
227
  field("logoCRC16", 0x15c, 2, "number");
224
228
  field("headerCRC16", 0x15e, 2, "number");