nitro-patcher 0.2.0 → 0.2.1
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.
- package/dist/browser.d.ts +5 -2
- package/dist/browser.js +1367 -1418
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-GFF67HX4.js → chunk-GLK3K2HY.js} +221 -282
- package/dist/chunk-GLK3K2HY.js.map +1 -0
- package/dist/{chunk-ZY3OSZCA.js → chunk-IGURDH5L.js} +180 -186
- package/dist/chunk-IGURDH5L.js.map +1 -0
- package/dist/cli.js +5 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/nitro-helper.d.ts +78 -78
- package/dist/nitro-helper.js +1 -1
- package/dist/nitro-patch-helper.d.ts +5 -2
- package/dist/nitro-patch-helper.js +4 -2
- package/package.json +6 -2
- package/src/browser/buffer.ts +1 -1
- package/src/browser.ts +16 -12
- package/src/cli.ts +38 -48
- package/src/index.ts +2 -2
- package/src/nitro/banner.ts +28 -31
- package/src/nitro/binary.ts +6 -16
- package/src/nitro/blz.ts +13 -21
- package/src/nitro/crypto.ts +78 -84
- package/src/nitro/filesystem.ts +36 -39
- package/src/nitro/header.ts +58 -71
- package/src/nitro/nds-file.ts +44 -63
- package/src/nitro/twl.ts +59 -100
- package/src/nitro-helper.ts +14 -14
- package/src/nitro-patch-helper.ts +49 -57
- package/src/patch/xdelta.ts +43 -49
- package/src/patch/zip.ts +47 -56
- package/src/runtime/browser.ts +9 -11
- package/src/runtime/node.ts +14 -15
- package/dist/chunk-GFF67HX4.js.map +0 -1
- package/dist/chunk-ZY3OSZCA.js.map +0 -1
package/src/nitro/header.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Port of NitroHelper/Header.cs. Copyright Xzonn and upstream contributors, GPL-3.0.
|
|
2
|
-
import { bytes, range } from
|
|
3
|
-
import { crc16 } from
|
|
4
|
-
import { encryptSecureArea } from
|
|
2
|
+
import { bytes, range } from "./binary";
|
|
3
|
+
import { crc16 } from "./crc";
|
|
4
|
+
import { encryptSecureArea } from "./crypto";
|
|
5
5
|
|
|
6
6
|
export const UnitCode = Object.freeze({ NDS: 0, NDS_DSi: 2, DSi: 3 });
|
|
7
7
|
|
|
@@ -106,43 +106,30 @@ export class Header {
|
|
|
106
106
|
declare secureDisable: bigint;
|
|
107
107
|
|
|
108
108
|
constructor(input: Uint8Array) {
|
|
109
|
-
const data = bytes(input,
|
|
110
|
-
range(data, 0, 0x170,
|
|
109
|
+
const data = bytes(input, "header");
|
|
110
|
+
range(data, 0, 0x170, "header");
|
|
111
111
|
const size = data.readUInt32LE(0x84);
|
|
112
112
|
if (size < 0x170 || size > data.length) throw new RangeError(`Invalid header size: ${size}`);
|
|
113
113
|
this.raw = Buffer.from(data.subarray(0, size));
|
|
114
|
-
if (this.unitCode & 2 && size > 0x200) range(this.raw, 0, 0x1000,
|
|
115
|
-
this.trimmedRom = this.total_rom_size
|
|
116
|
-
? this.total_rom_size >= data.length
|
|
117
|
-
: data.length !== this.size;
|
|
114
|
+
if (this.unitCode & 2 && size > 0x200) range(this.raw, 0, 0x1000, "DSi header");
|
|
115
|
+
this.trimmedRom = this.total_rom_size ? this.total_rom_size >= data.length : data.length !== this.size;
|
|
118
116
|
this.doublePadding =
|
|
119
|
-
(this.ARM9size % 0x400 < 0x200 &&
|
|
120
|
-
|
|
121
|
-
this.ARM9overlayOffset % 0x400 === 0) ||
|
|
122
|
-
(this.ARM7size % 0x400 < 0x200 &&
|
|
123
|
-
this.FNToffset % 0x400 === 0 &&
|
|
124
|
-
this.ARM7overlayOffset % 0x400 === 0) ||
|
|
117
|
+
(this.ARM9size % 0x400 < 0x200 && this.ARM7romOffset % 0x400 === 0 && this.ARM9overlayOffset % 0x400 === 0) ||
|
|
118
|
+
(this.ARM7size % 0x400 < 0x200 && this.FNToffset % 0x400 === 0 && this.ARM7overlayOffset % 0x400 === 0) ||
|
|
125
119
|
(this.FNTsize % 0x400 < 0x200 && this.FAToffset % 0x400 === 0) ||
|
|
126
120
|
(this.FATsize % 0x400 < 0x200 && this.bannerOffset % 0x400 === 0);
|
|
127
121
|
const nitroOffset = this.ARM9romOffset + this.ARM9size;
|
|
128
122
|
this.nitrocode =
|
|
129
|
-
nitroOffset >= size &&
|
|
130
|
-
nitroOffset + 4 <= data.length &&
|
|
131
|
-
data.readUInt32LE(nitroOffset) === 0xdec00621;
|
|
123
|
+
nitroOffset >= size && nitroOffset + 4 <= data.length && data.readUInt32LE(nitroOffset) === 0xdec00621;
|
|
132
124
|
this.decrypted = data.length >= 0x4008 && data.readBigUInt64LE(0x4000) === 0xe7ffdeffe7ffdeffn;
|
|
133
125
|
this.headerCRC = crc16(this.raw.subarray(0, 0x15e)) === this.headerCRC16;
|
|
134
126
|
this.logoCRC = crc16(this.logo) === this.logoCRC16;
|
|
135
127
|
const secure = data.subarray(0x4000, 0x8000);
|
|
136
128
|
this.secureCRC =
|
|
137
129
|
secure.length > 0 &&
|
|
138
|
-
crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) ===
|
|
139
|
-
this.secureCRC16;
|
|
130
|
+
crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) === this.secureCRC16;
|
|
140
131
|
this.dlp_signature = Buffer.alloc(0);
|
|
141
|
-
if (
|
|
142
|
-
this.ROMsize >= size &&
|
|
143
|
-
this.ROMsize + 0x88 <= data.length &&
|
|
144
|
-
data.readUInt32LE(this.ROMsize) === 0x00016361
|
|
145
|
-
) {
|
|
132
|
+
if (this.ROMsize >= size && this.ROMsize + 0x88 <= data.length && data.readUInt32LE(this.ROMsize) === 0x00016361) {
|
|
146
133
|
this.dlp_signature = Buffer.from(data.subarray(this.ROMsize, this.ROMsize + 0x88));
|
|
147
134
|
}
|
|
148
135
|
}
|
|
@@ -153,7 +140,7 @@ export class Header {
|
|
|
153
140
|
set size(value: number) {
|
|
154
141
|
const power = Math.log2(value);
|
|
155
142
|
if (!Number.isInteger(power) || power < 17 || power > 32)
|
|
156
|
-
throw new RangeError(
|
|
143
|
+
throw new RangeError("ROM capacity must be a power of two from 128 KiB to 4 GiB");
|
|
157
144
|
this.raw[0x14] = power - 17;
|
|
158
145
|
}
|
|
159
146
|
toBuffer() {
|
|
@@ -163,7 +150,7 @@ export class Header {
|
|
|
163
150
|
return data;
|
|
164
151
|
}
|
|
165
152
|
toString() {
|
|
166
|
-
return `NDS header: ${this.gameTitle.replace(/\0+$/,
|
|
153
|
+
return `NDS header: ${this.gameTitle.replace(/\0+$/, "")} (${this.gameCode})`;
|
|
167
154
|
}
|
|
168
155
|
}
|
|
169
156
|
|
|
@@ -171,44 +158,44 @@ function field(
|
|
|
171
158
|
name: string,
|
|
172
159
|
offset: number,
|
|
173
160
|
length: number,
|
|
174
|
-
type:
|
|
161
|
+
type: "buffer" | "string" | "number" = "buffer",
|
|
175
162
|
dsi = false,
|
|
176
163
|
) {
|
|
177
164
|
Object.defineProperty(Header.prototype, name, {
|
|
178
165
|
enumerable: true,
|
|
179
166
|
get(this: Header) {
|
|
180
167
|
if (dsi && (!(this.unitCode & 2) || this.raw.length <= 0x200))
|
|
181
|
-
return type ===
|
|
168
|
+
return type === "buffer" ? Buffer.alloc(length) : 0;
|
|
182
169
|
const data = range(this.raw, offset, length, `header ${name}`);
|
|
183
|
-
if (type ===
|
|
184
|
-
if (type ===
|
|
170
|
+
if (type === "string") return data.toString("latin1");
|
|
171
|
+
if (type === "buffer") return data;
|
|
185
172
|
return length === 8 ? data.readBigUInt64LE() : data.readUIntLE(0, length);
|
|
186
173
|
},
|
|
187
174
|
set(this: Header, value: string | number | bigint | Uint8Array) {
|
|
188
175
|
const target = range(this.raw, offset, length, `header ${name}`);
|
|
189
|
-
if (type ===
|
|
190
|
-
if (typeof value !==
|
|
176
|
+
if (type === "string") {
|
|
177
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "latin1") > length)
|
|
191
178
|
throw new TypeError(`Invalid ${name}`);
|
|
192
179
|
target.fill(0);
|
|
193
|
-
target.write(value,
|
|
194
|
-
} else if (type ===
|
|
180
|
+
target.write(value, "latin1");
|
|
181
|
+
} else if (type === "buffer") {
|
|
195
182
|
if (!(value instanceof Uint8Array)) throw new TypeError(`Invalid ${name}`);
|
|
196
183
|
const source = bytes(value, name);
|
|
197
184
|
if (source.length !== length) throw new RangeError(`${name} must contain ${length} bytes`);
|
|
198
185
|
source.copy(target);
|
|
199
186
|
} else if (length === 8) {
|
|
200
|
-
if (typeof value !==
|
|
187
|
+
if (typeof value !== "bigint") throw new TypeError(`Invalid ${name}`);
|
|
201
188
|
target.writeBigUInt64LE(value);
|
|
202
189
|
} else {
|
|
203
|
-
if (typeof value !==
|
|
190
|
+
if (typeof value !== "number") throw new TypeError(`Invalid ${name}`);
|
|
204
191
|
target.writeUIntLE(value, 0, length);
|
|
205
192
|
}
|
|
206
193
|
},
|
|
207
194
|
});
|
|
208
195
|
}
|
|
209
|
-
field(
|
|
210
|
-
field(
|
|
211
|
-
field(
|
|
196
|
+
field("gameTitle", 0, 12, "string");
|
|
197
|
+
field("gameCode", 12, 4, "string");
|
|
198
|
+
field("makerCode", 16, 2, "string");
|
|
212
199
|
for (const [name, offset] of Object.entries({
|
|
213
200
|
unitCode: 0x12,
|
|
214
201
|
encryptionSeed: 0x13,
|
|
@@ -217,38 +204,38 @@ for (const [name, offset] of Object.entries({
|
|
|
217
204
|
ROMversion: 0x1e,
|
|
218
205
|
internalFlags: 0x1f,
|
|
219
206
|
}))
|
|
220
|
-
field(name, offset, 1,
|
|
221
|
-
field(
|
|
207
|
+
field(name, offset, 1, "number");
|
|
208
|
+
field("reserved", 0x15, 7);
|
|
222
209
|
const ntr32 =
|
|
223
|
-
|
|
224
|
-
|
|
210
|
+
"ARM9romOffset ARM9entryAddress ARM9ramAddress ARM9size ARM7romOffset ARM7entryAddress ARM7ramAddress ARM7size FNToffset FNTsize FAToffset FATsize ARM9overlayOffset ARM9overlaySize ARM7overlayOffset ARM7overlaySize flagsRead flagsInit bannerOffset".split(
|
|
211
|
+
" ",
|
|
225
212
|
);
|
|
226
|
-
ntr32.forEach((name, i) => field(name, 0x20 + i * 4, 4,
|
|
227
|
-
field(
|
|
228
|
-
field(
|
|
229
|
-
field(
|
|
230
|
-
field(
|
|
231
|
-
field(
|
|
232
|
-
field(
|
|
233
|
-
field(
|
|
234
|
-
field(
|
|
235
|
-
field(
|
|
236
|
-
field(
|
|
237
|
-
field(
|
|
238
|
-
|
|
239
|
-
.split(
|
|
240
|
-
.forEach((name, i) => field(name, 0x160 + i * 4, 4,
|
|
241
|
-
field(
|
|
242
|
-
|
|
243
|
-
.split(
|
|
244
|
-
.forEach((name, i) => field(name, 0x1ac + i * 4, 4,
|
|
213
|
+
ntr32.forEach((name, i) => field(name, 0x20 + i * 4, 4, "number"));
|
|
214
|
+
field("secureCRC16", 0x6c, 2, "number");
|
|
215
|
+
field("ROMtimeout", 0x6e, 2, "number");
|
|
216
|
+
field("ARM9autoload", 0x70, 4, "number");
|
|
217
|
+
field("ARM7autoload", 0x74, 4, "number");
|
|
218
|
+
field("secureDisable", 0x78, 8, "number");
|
|
219
|
+
field("ROMsize", 0x80, 4, "number");
|
|
220
|
+
field("headerSize", 0x84, 4, "number");
|
|
221
|
+
field("reserved2", 0x88, 56);
|
|
222
|
+
field("logo", 0xc0, 156);
|
|
223
|
+
field("logoCRC16", 0x15c, 2, "number");
|
|
224
|
+
field("headerCRC16", 0x15e, 2, "number");
|
|
225
|
+
"debug_romOffset debug_size debug_ramAddress reserved3"
|
|
226
|
+
.split(" ")
|
|
227
|
+
.forEach((name, i) => field(name, 0x160 + i * 4, 4, "number"));
|
|
228
|
+
field("appflags", 0x1bc, 4, "buffer", true);
|
|
229
|
+
"mbk9_wramcnt_setting region_flags access_control scfg_ext_mask"
|
|
230
|
+
.split(" ")
|
|
231
|
+
.forEach((name, i) => field(name, 0x1ac + i * 4, 4, "number", true));
|
|
245
232
|
const twl32 =
|
|
246
|
-
|
|
247
|
-
|
|
233
|
+
"dsi9_rom_offset offset_0x1C4 dsi9_ram_address dsi9_size dsi7_rom_offset offset_0x1D4 dsi7_ram_address dsi7_size digest_ntr_start digest_ntr_size digest_twl_start digest_twl_size sector_hashtable_start sector_hashtable_size block_hashtable_start block_hashtable_size digest_sector_size digest_block_sectorcount banner_size offset_0x20C total_rom_size offset_0x214 offset_0x218 offset_0x21C modcrypt1_start modcrypt1_size modcrypt2_start modcrypt2_size tid_low tid_high public_sav_size private_sav_size".split(
|
|
234
|
+
" ",
|
|
248
235
|
);
|
|
249
|
-
twl32.forEach((name, i) => field(name, 0x1c0 + i * 4, 4,
|
|
250
|
-
|
|
251
|
-
.split(
|
|
252
|
-
.forEach((name, i) => field(name, 0x300 + i * 20, 20,
|
|
253
|
-
field(
|
|
254
|
-
field(
|
|
236
|
+
twl32.forEach((name, i) => field(name, 0x1c0 + i * 4, 4, "number", true));
|
|
237
|
+
"hmac_arm9 hmac_arm7 hmac_digest_master hmac_icon_title hmac_arm9i hmac_arm7i"
|
|
238
|
+
.split(" ")
|
|
239
|
+
.forEach((name, i) => field(name, 0x300 + i * 20, 20, "buffer", true));
|
|
240
|
+
field("hmac_arm9_no_secure", 0x3a0, 20, "buffer", true);
|
|
241
|
+
field("rsa_signature", 0xf80, 0x80, "buffer", true);
|
package/src/nitro/nds-file.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Native TypeScript port of NitroHelper/NDSFile.cs, GPL-3.0.
|
|
2
|
-
import { readFile, writeFile } from
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
2
|
+
import { readFile, writeFile } from "#nitro-runtime";
|
|
3
|
+
|
|
4
|
+
import { Banner } from "./banner";
|
|
5
|
+
import { BinaryWriter, DEFAULT_MAX_SIZE, bytes, range } from "./binary";
|
|
6
|
+
import { crc16 } from "./crc";
|
|
7
|
+
import { encryptSecureArea } from "./crypto";
|
|
8
|
+
import { FileAllocationTable, FileNameTable, OverlayTable } from "./filesystem";
|
|
9
|
+
import type { NitroFile, NitroFolder } from "./filesystem";
|
|
10
|
+
import { Header } from "./header";
|
|
11
|
+
import { TWL } from "./twl";
|
|
11
12
|
|
|
12
13
|
export interface RomOptions {
|
|
13
14
|
/** Maximum generated ROM size in bytes. Defaults to 1 GiB. */
|
|
@@ -22,7 +23,7 @@ export interface RomFileInfo {
|
|
|
22
23
|
size: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const key = (path: string): string => path.replaceAll(
|
|
26
|
+
const key = (path: string): string => path.replaceAll("\\", "/").toLowerCase();
|
|
26
27
|
|
|
27
28
|
/** Parsed Nintendo DS ROM. Replacements do not change the input buffer. */
|
|
28
29
|
export class NDSFile {
|
|
@@ -40,31 +41,21 @@ export class NDSFile {
|
|
|
40
41
|
private readonly maxOutputSize: number;
|
|
41
42
|
|
|
42
43
|
constructor(input: Uint8Array, options: RomOptions = {}) {
|
|
43
|
-
this.original = bytes(input,
|
|
44
|
+
this.original = bytes(input, "ROM");
|
|
44
45
|
this.maxOutputSize = options.maxOutputSize ?? DEFAULT_MAX_SIZE;
|
|
45
46
|
this.header = new Header(this.original);
|
|
46
47
|
const h = this.header;
|
|
47
|
-
range(this.original, h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0),
|
|
48
|
-
range(this.original, h.ARM7romOffset, h.ARM7size,
|
|
48
|
+
range(this.original, h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0), "ARM9");
|
|
49
|
+
range(this.original, h.ARM7romOffset, h.ARM7size, "ARM7");
|
|
49
50
|
this.banner = new Banner(this.original, h.bannerOffset, h.banner_size);
|
|
50
51
|
this.fatTable = new FileAllocationTable(this.original, h.FAToffset, h.FATsize);
|
|
51
52
|
this.fntTable = new FileNameTable(this.fatTable, this.original, h.FNToffset, h.FNTsize);
|
|
52
53
|
this.root = this.fntTable.root;
|
|
53
|
-
this.overlay9Table = new OverlayTable(
|
|
54
|
-
|
|
55
|
-
h.ARM9overlayOffset,
|
|
56
|
-
h.ARM9overlaySize,
|
|
57
|
-
true,
|
|
58
|
-
);
|
|
59
|
-
this.overlay7Table = new OverlayTable(
|
|
60
|
-
this.original,
|
|
61
|
-
h.ARM7overlayOffset,
|
|
62
|
-
h.ARM7overlaySize,
|
|
63
|
-
false,
|
|
64
|
-
);
|
|
54
|
+
this.overlay9Table = new OverlayTable(this.original, h.ARM9overlayOffset, h.ARM9overlaySize, true);
|
|
55
|
+
this.overlay7Table = new OverlayTable(this.original, h.ARM7overlayOffset, h.ARM7overlaySize, false);
|
|
65
56
|
const arm9Overlays = this.overlay9Table.readBasicOverlays(this.fatTable);
|
|
66
57
|
this.root.folders.push({
|
|
67
|
-
name:
|
|
58
|
+
name: "overlay",
|
|
68
59
|
id: 0xffff,
|
|
69
60
|
files: [...arm9Overlays, ...this.overlay7Table.readBasicOverlays(this.fatTable)],
|
|
70
61
|
folders: [],
|
|
@@ -73,23 +64,23 @@ export class NDSFile {
|
|
|
73
64
|
range(this.original, offset, size, name);
|
|
74
65
|
this.root.files.push({ name, id: 0xffff, offset, size });
|
|
75
66
|
};
|
|
76
|
-
system(
|
|
77
|
-
system(
|
|
78
|
-
system(
|
|
79
|
-
system(
|
|
80
|
-
system(
|
|
81
|
-
system(
|
|
82
|
-
if (h.ARM9overlaySize) system(
|
|
83
|
-
if (h.ARM7overlaySize) system(
|
|
67
|
+
system("header.bin", 0, h.headerSize);
|
|
68
|
+
system("banner.bin", h.bannerOffset, this.banner.raw.length);
|
|
69
|
+
system("fnt.bin", h.FNToffset, h.FNTsize);
|
|
70
|
+
system("fat.bin", h.FAToffset, h.FATsize);
|
|
71
|
+
system("arm9.bin", h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0));
|
|
72
|
+
system("arm7.bin", h.ARM7romOffset, h.ARM7size);
|
|
73
|
+
if (h.ARM9overlaySize) system("overarm9.bin", h.ARM9overlayOffset, h.ARM9overlaySize);
|
|
74
|
+
if (h.ARM7overlaySize) system("overarm7.bin", h.ARM7overlayOffset, h.ARM7overlaySize);
|
|
84
75
|
const visit = (folder: NitroFolder, prefix: string): void => {
|
|
85
76
|
for (const file of folder.files) {
|
|
86
77
|
const path = prefix + file.name;
|
|
87
78
|
if (this.paths.has(key(path))) throw new Error(`Duplicate ROM path: ${path}`);
|
|
88
79
|
this.paths.set(key(path), { path, file });
|
|
89
80
|
}
|
|
90
|
-
for (const child of folder.folders) visit(child, prefix + child.name
|
|
81
|
+
for (const child of folder.folders) visit(child, `${prefix + child.name}/`);
|
|
91
82
|
};
|
|
92
|
-
visit(this.root,
|
|
83
|
+
visit(this.root, "");
|
|
93
84
|
if (h.unitCode & 2 && h.twlInternalFlags & 1 && h.tid_high && h.tid_high !== 0xffffffff) {
|
|
94
85
|
// Upstream silently discarded malformed TWL regions. Preserve valid DSi data,
|
|
95
86
|
// and reject malformed data instead of emitting a ROM with missing sections.
|
|
@@ -135,20 +126,13 @@ export class NDSFile {
|
|
|
135
126
|
|
|
136
127
|
/** Rebuild offsets, allocation table, CRCs and ROM padding. Safe to call repeatedly. */
|
|
137
128
|
toBuffer(): Buffer {
|
|
138
|
-
const fntReplacement = this.root.files.find((file) => file.name ===
|
|
129
|
+
const fntReplacement = this.root.files.find((file) => file.name === "fnt.bin")?.replacement;
|
|
139
130
|
if (fntReplacement) {
|
|
140
|
-
const replacementTree = new FileNameTable(
|
|
141
|
-
this.fatTable,
|
|
142
|
-
fntReplacement,
|
|
143
|
-
0,
|
|
144
|
-
fntReplacement.length,
|
|
145
|
-
);
|
|
131
|
+
const replacementTree = new FileNameTable(this.fatTable, fntReplacement, 0, fntReplacement.length);
|
|
146
132
|
const collect = (folder: NitroFolder): number[] =>
|
|
147
|
-
[...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort(
|
|
148
|
-
|
|
149
|
-
);
|
|
150
|
-
if (collect(replacementTree.root).join(',') !== collect(this.data).join(','))
|
|
151
|
-
throw new Error('Replacement FNT must preserve existing file IDs');
|
|
133
|
+
[...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort((a, b) => a - b);
|
|
134
|
+
if (collect(replacementTree.root).join(",") !== collect(this.data).join(","))
|
|
135
|
+
throw new Error("Replacement FNT must preserve existing file IDs");
|
|
152
136
|
}
|
|
153
137
|
const h = new Header(this.header.toBuffer());
|
|
154
138
|
h.nitrocode = this.header.nitrocode;
|
|
@@ -161,7 +145,7 @@ export class NDSFile {
|
|
|
161
145
|
if (!file) throw new Error(`Missing ROM system file: ${name}`);
|
|
162
146
|
return file;
|
|
163
147
|
};
|
|
164
|
-
const replacedHeader = system(
|
|
148
|
+
const replacedHeader = system("header.bin").replacement;
|
|
165
149
|
if (replacedHeader) {
|
|
166
150
|
const replacement = new Header(replacedHeader);
|
|
167
151
|
h.gameTitle = replacement.gameTitle;
|
|
@@ -177,13 +161,13 @@ export class NDSFile {
|
|
|
177
161
|
const overlayIds = new Set<number>();
|
|
178
162
|
const writeOverlays = (arm9: boolean): void => {
|
|
179
163
|
const originalTable = arm9 ? this.overlay9Table : this.overlay7Table;
|
|
180
|
-
const name = arm9 ?
|
|
164
|
+
const name = arm9 ? "overarm9.bin" : "overarm7.bin";
|
|
181
165
|
const entry = this.root.files.find((file) => file.name === name);
|
|
182
166
|
if (!entry) return;
|
|
183
167
|
const raw = entry.replacement ?? originalTable.toBuffer();
|
|
184
168
|
const table = new OverlayTable(raw, 0, raw.length, arm9);
|
|
185
169
|
const files = this.overlay.files
|
|
186
|
-
.filter((file) => file.name.startsWith(arm9 ?
|
|
170
|
+
.filter((file) => file.name.startsWith(arm9 ? "overlay_" : "overlay7_"))
|
|
187
171
|
.sort((a, b) => a.id - b.id);
|
|
188
172
|
for (const file of files) {
|
|
189
173
|
const item = table.entries.find((item) => item.fileId === file.id);
|
|
@@ -193,8 +177,7 @@ export class NDSFile {
|
|
|
193
177
|
item.reserved = ((item.reserved & 0xff000000) | (size & 0xffffff)) >>> 0;
|
|
194
178
|
overlayIds.add(file.id);
|
|
195
179
|
}
|
|
196
|
-
if (table.entries.length !== files.length)
|
|
197
|
-
throw new Error('Changing overlay file count is unsupported');
|
|
180
|
+
if (table.entries.length !== files.length) throw new Error("Changing overlay file count is unsupported");
|
|
198
181
|
const encoded = table.toBuffer();
|
|
199
182
|
if (arm9) {
|
|
200
183
|
h.ARM9overlayOffset = writer.position;
|
|
@@ -209,30 +192,29 @@ export class NDSFile {
|
|
|
209
192
|
};
|
|
210
193
|
writer.position = h.headerSize;
|
|
211
194
|
h.ARM9romOffset = writer.position;
|
|
212
|
-
const arm9 = system(
|
|
195
|
+
const arm9 = system("arm9.bin");
|
|
213
196
|
const arm9Bytes = this.fileBytes(arm9);
|
|
214
197
|
if (arm9.replacement) {
|
|
215
|
-
h.nitrocode =
|
|
216
|
-
arm9Bytes.length >= 12 && arm9Bytes.readUInt32LE(arm9Bytes.length - 12) === 0xdec00621;
|
|
198
|
+
h.nitrocode = arm9Bytes.length >= 12 && arm9Bytes.readUInt32LE(arm9Bytes.length - 12) === 0xdec00621;
|
|
217
199
|
}
|
|
218
200
|
h.ARM9size = arm9Bytes.length - (h.nitrocode ? 12 : 0);
|
|
219
201
|
write(arm9);
|
|
220
202
|
h.ARM9overlayOffset = 0;
|
|
221
203
|
writeOverlays(true);
|
|
222
204
|
h.ARM7romOffset = writer.position;
|
|
223
|
-
h.ARM7size = this.fileBytes(system(
|
|
224
|
-
write(system(
|
|
205
|
+
h.ARM7size = this.fileBytes(system("arm7.bin")).length;
|
|
206
|
+
write(system("arm7.bin"));
|
|
225
207
|
h.ARM7overlayOffset = 0;
|
|
226
208
|
writeOverlays(false);
|
|
227
209
|
h.FNToffset = writer.position;
|
|
228
|
-
h.FNTsize = this.fileBytes(system(
|
|
229
|
-
write(system(
|
|
210
|
+
h.FNTsize = this.fileBytes(system("fnt.bin")).length;
|
|
211
|
+
write(system("fnt.bin"));
|
|
230
212
|
h.FAToffset = writer.position;
|
|
231
213
|
h.FATsize = this.fatTable.entries.length * 8;
|
|
232
214
|
writer.write(Buffer.alloc(h.FATsize));
|
|
233
215
|
writer.pad(0x200);
|
|
234
216
|
h.bannerOffset = writer.position;
|
|
235
|
-
const bannerFile = system(
|
|
217
|
+
const bannerFile = system("banner.bin");
|
|
236
218
|
const banner = bannerFile.replacement
|
|
237
219
|
? new Banner(bannerFile.replacement, 0, bannerFile.replacement.length)
|
|
238
220
|
: this.banner;
|
|
@@ -265,8 +247,7 @@ export class NDSFile {
|
|
|
265
247
|
writer.position = logicalEnd;
|
|
266
248
|
// An ARM9 replacement can add or remove the decrypted secure-area marker.
|
|
267
249
|
// Both TWL hashing and the header CRC must use the bytes being serialized.
|
|
268
|
-
h.decrypted =
|
|
269
|
-
writer.length >= 0x4008 && writer.buffer.readBigUInt64LE(0x4000) === 0xe7ffdeffe7ffdeffn;
|
|
250
|
+
h.decrypted = writer.length >= 0x4008 && writer.buffer.readBigUInt64LE(0x4000) === 0xe7ffdeffe7ffdeffn;
|
|
270
251
|
if (this.twl) this.twl.writeTo(writer, h);
|
|
271
252
|
h.ROMsize = writer.position;
|
|
272
253
|
h.size = 2 ** Math.ceil(Math.log2(Math.max(h.ROMsize + h.dlp_signature.length, 0x20000)));
|