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/banner.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Port of NitroHelper/Banner.cs. Copyright Xzonn and upstream contributors, GPL-3.0.
|
|
2
|
-
import { bytes, range } from
|
|
3
|
-
import { crc16 } from
|
|
2
|
+
import { bytes, range } from "./binary";
|
|
3
|
+
import { crc16 } from "./crc";
|
|
4
4
|
|
|
5
5
|
const titles = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
"japaneseTitle",
|
|
7
|
+
"englishTitle",
|
|
8
|
+
"frenchTitle",
|
|
9
|
+
"germanTitle",
|
|
10
|
+
"italianTitle",
|
|
11
|
+
"spanishTitle",
|
|
12
|
+
"chineseTitle",
|
|
13
|
+
"koreanTitle",
|
|
14
14
|
] as const;
|
|
15
15
|
export class Banner {
|
|
16
16
|
version: number;
|
|
@@ -18,21 +18,20 @@ export class Banner {
|
|
|
18
18
|
bannerCRC: boolean;
|
|
19
19
|
tileData: Buffer;
|
|
20
20
|
palette: Buffer;
|
|
21
|
-
japaneseTitle =
|
|
22
|
-
englishTitle =
|
|
23
|
-
frenchTitle =
|
|
24
|
-
germanTitle =
|
|
25
|
-
italianTitle =
|
|
26
|
-
spanishTitle =
|
|
27
|
-
chineseTitle =
|
|
28
|
-
koreanTitle =
|
|
21
|
+
japaneseTitle = "";
|
|
22
|
+
englishTitle = "";
|
|
23
|
+
frenchTitle = "";
|
|
24
|
+
germanTitle = "";
|
|
25
|
+
italianTitle = "";
|
|
26
|
+
spanishTitle = "";
|
|
27
|
+
chineseTitle = "";
|
|
28
|
+
koreanTitle = "";
|
|
29
29
|
constructor(input: Uint8Array, offset = 0, hardSize = 0) {
|
|
30
|
-
const data = bytes(input,
|
|
31
|
-
this.version = range(data, offset, 2,
|
|
30
|
+
const data = bytes(input, "banner");
|
|
31
|
+
this.version = range(data, offset, 2, "banner").readUInt16LE();
|
|
32
32
|
let size = this.getDefSize(hardSize);
|
|
33
|
-
if (this.version >>> 8 === 1)
|
|
34
|
-
|
|
35
|
-
this.raw = Buffer.from(range(data, offset, size, 'banner'));
|
|
33
|
+
if (this.version >>> 8 === 1) size = hardSize > 0 && hardSize !== 0xffffffff ? Math.min(hardSize, 0x23c0) : 0x23c0;
|
|
34
|
+
this.raw = Buffer.from(range(data, offset, size, "banner"));
|
|
36
35
|
this.bannerCRC = crc16(this.raw.subarray(0x20, 0x840)) === this.raw.readUInt16LE(2);
|
|
37
36
|
this.tileData = Buffer.from(this.raw.subarray(0x20, 0x220));
|
|
38
37
|
this.palette = Buffer.from(this.raw.subarray(0x220, 0x240));
|
|
@@ -41,9 +40,9 @@ export class Banner {
|
|
|
41
40
|
i < this.titleCount
|
|
42
41
|
? this.raw
|
|
43
42
|
.subarray(0x240 + i * 0x100, 0x340 + i * 0x100)
|
|
44
|
-
.toString(
|
|
45
|
-
.split(
|
|
46
|
-
:
|
|
43
|
+
.toString("utf16le")
|
|
44
|
+
.split("\0")[0]
|
|
45
|
+
: "";
|
|
47
46
|
}
|
|
48
47
|
get titleCount() {
|
|
49
48
|
return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
|
|
@@ -57,14 +56,12 @@ export class Banner {
|
|
|
57
56
|
toBuffer() {
|
|
58
57
|
const output = Buffer.from(this.raw);
|
|
59
58
|
output.writeUInt16LE(this.version);
|
|
60
|
-
if (this.tileData.length !== 512 || this.palette.length !== 32)
|
|
61
|
-
throw new RangeError('Invalid banner icon size');
|
|
59
|
+
if (this.tileData.length !== 512 || this.palette.length !== 32) throw new RangeError("Invalid banner icon size");
|
|
62
60
|
this.tileData.copy(output, 0x20);
|
|
63
61
|
this.palette.copy(output, 0x220);
|
|
64
62
|
for (let i = 0; i < this.titleCount; i++) {
|
|
65
|
-
const text = Buffer.from(this[titles[i]],
|
|
66
|
-
if (text.length > 256)
|
|
67
|
-
throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
63
|
+
const text = Buffer.from(this[titles[i]], "utf16le");
|
|
64
|
+
if (text.length > 256) throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
68
65
|
output.fill(0, 0x240 + i * 256, 0x340 + i * 256);
|
|
69
66
|
text.copy(output, 0x240 + i * 256);
|
|
70
67
|
}
|
package/src/nitro/binary.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
export const DEFAULT_MAX_SIZE = 0x40000000;
|
|
2
2
|
|
|
3
|
-
export const bytes = (value: Uint8Array, name =
|
|
3
|
+
export const bytes = (value: Uint8Array, name = "input"): Buffer => {
|
|
4
4
|
if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Buffer or Uint8Array`);
|
|
5
5
|
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export const range = (
|
|
9
|
-
buffer: Buffer,
|
|
10
|
-
offset: number,
|
|
11
|
-
size: number,
|
|
12
|
-
name = 'binary data',
|
|
13
|
-
): Buffer => {
|
|
8
|
+
export const range = (buffer: Buffer, offset: number, size: number, name = "binary data"): Buffer => {
|
|
14
9
|
if (
|
|
15
10
|
!Number.isSafeInteger(offset) ||
|
|
16
11
|
!Number.isSafeInteger(size) ||
|
|
@@ -18,16 +13,12 @@ export const range = (
|
|
|
18
13
|
size < 0 ||
|
|
19
14
|
offset + size > buffer.length
|
|
20
15
|
) {
|
|
21
|
-
throw new RangeError(
|
|
22
|
-
`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`,
|
|
23
|
-
);
|
|
16
|
+
throw new RangeError(`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`);
|
|
24
17
|
}
|
|
25
18
|
return buffer.subarray(offset, offset + size);
|
|
26
19
|
};
|
|
27
20
|
|
|
28
|
-
export const align = (value: number, alignment = 0x200): number =>
|
|
29
|
-
return Math.ceil(value / alignment) * alignment;
|
|
30
|
-
};
|
|
21
|
+
export const align = (value: number, alignment = 0x200): number => Math.ceil(value / alignment) * alignment;
|
|
31
22
|
|
|
32
23
|
/** Seekable, bounded in-memory writer; unwritten gaps have MemoryStream's zero fill. */
|
|
33
24
|
export class BinaryWriter {
|
|
@@ -37,7 +28,7 @@ export class BinaryWriter {
|
|
|
37
28
|
length: number;
|
|
38
29
|
constructor(initialSize = 65536, maxSize = DEFAULT_MAX_SIZE) {
|
|
39
30
|
if (!Number.isSafeInteger(maxSize) || maxSize < 1 || maxSize > 0xffffffff)
|
|
40
|
-
throw new RangeError(
|
|
31
|
+
throw new RangeError("Invalid output size limit");
|
|
41
32
|
this.maxSize = maxSize;
|
|
42
33
|
this.buffer = Buffer.alloc(Math.min(initialSize, maxSize));
|
|
43
34
|
this.position = 0;
|
|
@@ -55,8 +46,7 @@ export class BinaryWriter {
|
|
|
55
46
|
|
|
56
47
|
write(data: Uint8Array) {
|
|
57
48
|
data = bytes(data);
|
|
58
|
-
if (!Number.isSafeInteger(this.position) || this.position < 0)
|
|
59
|
-
throw new RangeError('Invalid writer position');
|
|
49
|
+
if (!Number.isSafeInteger(this.position) || this.position < 0) throw new RangeError("Invalid writer position");
|
|
60
50
|
this.ensure(this.position + data.length);
|
|
61
51
|
bytes(data).copy(this.buffer, this.position);
|
|
62
52
|
this.position += data.length;
|
package/src/nitro/blz.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface Arm9Header {
|
|
|
7
7
|
// Native port of Xzonn/BlzHelper BLZ.cs (MIT), including overwrite-safe prefix.
|
|
8
8
|
// Compression returns an empty Buffer when no smaller representation exists.
|
|
9
9
|
export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
10
|
-
if (input.length < 8) throw new Error(
|
|
10
|
+
if (input.length < 8) throw new Error("BLZ footer is truncated");
|
|
11
11
|
const diff = input.readUInt32LE(input.length - 4);
|
|
12
12
|
const packed = input.readUInt32LE(input.length - 8),
|
|
13
13
|
header = packed >>> 24,
|
|
@@ -15,7 +15,7 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
|
15
15
|
const size = input.length + diff,
|
|
16
16
|
prefix = input.length - length;
|
|
17
17
|
if (header < 8 || header > length || length > input.length || size > maxSize)
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error("Invalid BLZ size or footer");
|
|
19
19
|
const out = Buffer.alloc(size);
|
|
20
20
|
input.copy(out, 0, 0, prefix);
|
|
21
21
|
let src = input.length - header,
|
|
@@ -24,16 +24,15 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
|
24
24
|
const flags = input[--src];
|
|
25
25
|
for (let bit = 128; bit && src > prefix; bit >>= 1) {
|
|
26
26
|
if (!(flags & bit)) {
|
|
27
|
-
if (dst <= prefix) throw new Error(
|
|
27
|
+
if (dst <= prefix) throw new Error("BLZ output overflow");
|
|
28
28
|
out[--dst] = input[--src];
|
|
29
29
|
} else {
|
|
30
|
-
if (src - 2 < prefix) throw new Error(
|
|
30
|
+
if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
|
|
31
31
|
const high = input[--src],
|
|
32
32
|
low = input[--src];
|
|
33
33
|
const count = (high >>> 4) + 3,
|
|
34
34
|
offset = (((high & 15) << 8) | low) + 3;
|
|
35
|
-
if (dst - count < prefix || dst - 1 + offset >= size)
|
|
36
|
-
throw new Error('Invalid BLZ reference');
|
|
35
|
+
if (dst - count < prefix || dst - 1 + offset >= size) throw new Error("Invalid BLZ reference");
|
|
37
36
|
for (let j = 0; j < count; j++) {
|
|
38
37
|
--dst;
|
|
39
38
|
out[dst] = out[dst + offset];
|
|
@@ -41,12 +40,12 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
if (dst !== prefix) throw new Error(
|
|
43
|
+
if (dst !== prefix) throw new Error("BLZ decompressed size mismatch");
|
|
45
44
|
return out;
|
|
46
45
|
};
|
|
47
46
|
|
|
48
47
|
export const compressBLZ = (input: Buffer): Buffer => {
|
|
49
|
-
if (input.length > 0xffffff) throw new Error(
|
|
48
|
+
if (input.length > 0xffffff) throw new Error("BLZ input exceeds 24-bit format limit");
|
|
50
49
|
const temp = Buffer.alloc(input.length);
|
|
51
50
|
let src = input.length,
|
|
52
51
|
dst = input.length;
|
|
@@ -131,31 +130,24 @@ export const compressBLZ = (input: Buffer): Buffer => {
|
|
|
131
130
|
return out;
|
|
132
131
|
};
|
|
133
132
|
|
|
134
|
-
export const decompressArm9 = (
|
|
135
|
-
data
|
|
136
|
-
header: Arm9Header,
|
|
137
|
-
): { compressed: boolean; data: Buffer } => {
|
|
138
|
-
if (data.length < 0x18) throw new Error('ARM9 data too short');
|
|
133
|
+
export const decompressArm9 = (data: Buffer, header: Arm9Header): { compressed: boolean; data: Buffer } => {
|
|
134
|
+
if (data.length < 0x18) throw new Error("ARM9 data too short");
|
|
139
135
|
const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
|
|
140
136
|
const init = header.reserved2.readUInt32LE(0) & 0x3fff;
|
|
141
137
|
const ptr = init ? data.readUInt32LE(init + 0x14) : header.ARM9ramAddress + header.ARM9size;
|
|
142
|
-
const compressed =
|
|
143
|
-
ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
|
|
138
|
+
const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
|
|
144
139
|
return {
|
|
145
140
|
compressed,
|
|
146
141
|
data: compressed
|
|
147
|
-
? Buffer.concat([
|
|
148
|
-
decompressBLZ(data.subarray(0, data.length - nitro)),
|
|
149
|
-
data.subarray(data.length - nitro),
|
|
150
|
-
])
|
|
142
|
+
? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)])
|
|
151
143
|
: Buffer.from(data),
|
|
152
144
|
};
|
|
153
145
|
};
|
|
154
146
|
export const compressArm9 = (data: Buffer, header: Arm9Header, postSize = 0): Buffer => {
|
|
155
|
-
if (data.length < 0x4000) throw new Error(
|
|
147
|
+
if (data.length < 0x4000) throw new Error("ARM9 secure area is truncated");
|
|
156
148
|
const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
|
|
157
149
|
const packed = compressBLZ(data.subarray(0x4000, data.length - nitro));
|
|
158
|
-
if (!packed.length) throw new Error(
|
|
150
|
+
if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
|
|
159
151
|
const out = Buffer.concat([data.subarray(0, 0x4000), packed, data.subarray(data.length - nitro)]);
|
|
160
152
|
const init = header.reserved2.readUInt32LE(0) & 0x3fff;
|
|
161
153
|
if (init) out.writeUInt32LE(out.length - postSize + header.ARM9ramAddress, init + 0x14);
|
package/src/nitro/crypto.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAesBlockCipher } from
|
|
1
|
+
import { createAesBlockCipher } from "#nitro-runtime";
|
|
2
2
|
|
|
3
3
|
export interface ModcryptHeader {
|
|
4
4
|
gameCode: string;
|
|
@@ -10,78 +10,77 @@ export interface ModcryptHeader {
|
|
|
10
10
|
|
|
11
11
|
// KEY1 table carried forward from NitroHelper/Arm9Encryptor.cs (Dirbaio/ndstool).
|
|
12
12
|
const KEY1 = Buffer.from(
|
|
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
|
-
|
|
13
|
+
"99D5205F5744F5B96E19A4D99E6A5A94D8AEF1EB4175E23A9382D03233EE31D5CC57619A3706A21B793972F555AEF6BE5F1B69FBE59DF1E9CE2CD9A15E3205E6" +
|
|
14
|
+
"FED3FECFD462040D8BF5ECB72B6079BB1295310D6E3FDA2B8884F0F13D127E254522F1BB24061A0611ADDF288B6481342BEB332999AAF2BD9C14959D9FF7F58C" +
|
|
15
|
+
"7297A1299DD15FCF664D071ADED34A4B85C9A7A31795053A3D490ABF0A898BA24A8249DD2790F10BE9EB1C6A83764505BA817061173F4BDEAECFAB3957F23A56" +
|
|
16
|
+
"4811AD8A40E1453FFA9B0254CAA693FBEF4DFE6FA3D8879C08BAD5486A8D2DFD6E15F874BDBE528B18228A9EFB7437071B366C4A19BA4262B97991107B676596" +
|
|
17
|
+
"FE0223E8EE998C773E5C86644D6D7886A54F65E21EB2DF5A0AD07E0814B071ACBDDB831CB9D7A162CDC6637C5269C3E6BF75CE12445D2104FAFBD33C381163D4" +
|
|
18
|
+
"958541494609F2084311DC1F76C0156D1F3C6370EA87806CC3BD638BC2372137DCEE09232E376A4D7390F75030AC1C92041023914FD207AA683E4F9AC964606A" +
|
|
19
|
+
"C81421F3D62241124424CFE68A56DD0D534DE1851E8C525A9C1984C20357F16FE300BE58F64CEDD521649C1FBE55033C4ADCFFAAC9DAE05D5EBFE6DEF5D8B1F8" +
|
|
20
|
+
"FF36B3B9626795DB315F37ED4C70679990B518316C3D9999E442DAD3254213A0AED7706CB155CFC7D746D54361173D4428E93385D5D0A293AA25121FFBC50B46" +
|
|
21
|
+
"F597765645A6BE87B1946BE8B1FE3399AE1F3E6C39711D09009037E4103E7574FF8C833BB0F1B0F90105474295F1D6AC7E38E69E9574263FB4685018D04330B4" +
|
|
22
|
+
"4C4BE368BFE54DB6958B0AA074253277CFA1F72CD871135AABEAC951E80DEEEFE9937E19A71E433881162CA148E373CC29216CD35DCEA0D9617143A01513B564" +
|
|
23
|
+
"92CF2A19DCADB7A59F8665F81A9FE7FBF7FDB8136C27DB6FDF351CF78D2C5B9B12AB386406CCDE31E84E751164E3FAEAEB3454C2AD3F34EB932C7D26369D56F3" +
|
|
24
|
+
"5AE1F6B398634A9E3283E49A84607D902E130EEE934B36A285EC1638E8880602BFF0A03AEDD76A9A73E157CFF844B8DC2E2359D1DF9552719961A04BD57F6E78" +
|
|
25
|
+
"BAA9C530D34086329D320C9C37B7022FBA5498A9C41304C98DBEC8E75D97502E93D622590C27BC2292E0A7200F936F7F4C9FD3B5A62A0B7467497D1026CBD1C5" +
|
|
26
|
+
"8671E78CA09CE95BB21AF601EE8C9E5E83F21ADBE6E5EA845976D27CF68DA5493648C21652BB83A374B9070C3BFF6128E161E9E4EF6E15AA4EBAE85D0596BB32" +
|
|
27
|
+
"56B0FB72520F0EC84225657689AFF2DE1027F0014B74A79707D5265454091F820A867D30390EB3269B0B57BB360631AFFD79FCD930102B0CB3E19BD77BDC5FEF" +
|
|
28
|
+
"D2F813454D4775BD46963C7E75F33EB567C59A3BB05B296BDE805BC81505B131B6CE49DDAD84B5AE60DC67313430FE4EBD802FA6BF63392186D9357F16682205" +
|
|
29
|
+
"54E990268C076C51A43155D70907A83E2E5366C1F8F27BC4F258CFF187C5A2E7278F308758A064622318B9887CFACEC498AEAD17CC4A5BF3E948D556D30DF2C8" +
|
|
30
|
+
"92738CDBD72F56AC81F992694DC632F6E6C08D21E276806111BCDC6C93AF19699BD0BFB9319F0267A351EE8306227B0CAB494240B8D5017DCE5EF7555339C599" +
|
|
31
|
+
"46D8879FBAF764B4E39AFAA16D90681030CA8A54A79F60C319F56B0D7A5198E6984351B4D635E94FC3DF0F7BD62F5CBD3A156119F14BCBAADC6D64C9D3C61E56" +
|
|
32
|
+
"EF384C50718675CC0D0D4EE928F6065D701BAAD345CFA839AC95A62EB4E422D474A8375F487A04CCA54C40D828B428080D1C725241F07D47193A534E5884626B" +
|
|
33
|
+
"93B58A81214E0DDCB43FA2C6FCC92B40DA3804E95E5A866B0C22258568118D7C921D95554DAB8EBBDAA6E6B751B6325A0541DD052A0A5650911747CCC9E67EB5" +
|
|
34
|
+
"614ADB736751C833F5DA6E742E54C3370D6DAF08E8158A5FE25921CDA8DE0C065A776B5FDB18653EC850DE78E0B882B35D4E7232074FC13423BA96B7674EA428" +
|
|
35
|
+
"1E3462EB2D6A70E92F42C4704E5A319CF95B4728AADA716F381FB378C4926B1C9EF6359AB74D0EBFCC1829410348355D55D02BC629AF5C6074698E5E9B7CD4BD" +
|
|
36
|
+
"7B44647D3F925D69B61F004BD48335CF7E644E17AE8DD52E9A28124E2E2B49085CAEC64685AE41611E6F82D25137161F0BF659A49ACA5AAF0DD4338B2063F184" +
|
|
37
|
+
"805CCBCF08B4B9D31605BD6283319B5651989FBAB25BAAB2226B2CB5D448FA632B5F58FA61FA6409BB38E0B89D9260A80D676F0E37F50D019FC277D4FEECF173" +
|
|
38
|
+
"3039E07DF56198E42C2855045655DB2F6BECE55806B664806A2A1A4E5B0FD8C40A2E5219D962F53048BE8C7B4F389BA2C3AFC9D3C7C1624186B96121576F994F" +
|
|
39
|
+
"C1BACE7BB53B4D5E8A8B44575F135F706D5B2947DC38E2EC045565122AE81743E18EDD2AB3E294F7096E5CE6EB8AF86D89495448F52FADBFEA944BCAFC398782" +
|
|
40
|
+
"5F8A01F275F2E671D6D842DEF12D1D28A6887EA3A0471D30D9A371DF491CCB01F836B1F2F022585D456BBDA0BBB28842C78C28CE93E8906308907C893CF57DB7" +
|
|
41
|
+
"042D4F555116FD7E79E8BEC1F212D4F8B4840523A0CCD22BFDE1ABAD0DD1556C2341944D77374F05280CBF17B312676C8CC35AF741842A6DD09412272CB4ED9C" +
|
|
42
|
+
"4DEC478297D567B91B9DC055077EE58EE2A8E73E12E40E3A2A455534A2F92D5A1BAB527C83105F55D2F15A432BC6A7A4891595E8B44B9DF875E39F60785BD6E6" +
|
|
43
|
+
"0D44E62106BD472253A400AD8D43138539F7AAFC38AF7BEDFCE42B5450984CFC8580F7DF3C8022E194DADE24C6B07A3938DC0FA1A7F4F96F6318578B84412A2E" +
|
|
44
|
+
"D453F2D9000FD0DD996E19A60AD0EC5B5824ABC0CB0665EC1A1338940A67032F3FF7E377447733C61439D0E3C0A20879BB409957410B0190CDE1CC4867DBB3AF" +
|
|
45
|
+
"8874F34C828F72B1B52329C4126C19FC8E46A49CC4256587D36DBE8A93110338ED832BF346A493EA3B53851DCED4F1088327EDFC9B1A18BCF98BAEDC24AB5038" +
|
|
46
|
+
"E9724B1022177B465DAB5964F340AEF8BBE5C8F926034E557DEBEBFEF739E6E00A11BE2E28FF98EDC0C9425642C3FD00F6AF87A25B013F329247959A72A5323D" +
|
|
47
|
+
"AE6BD09B07D24992E3784AFAA1067DF241CF77740414B20C86846416D5BB51A1E56FF1D1F2E2F75F58204DB857C7CFDDC5D8BE763DF65F7EE72A8B88241B383F" +
|
|
48
|
+
"0E412377F5F04BD40C1FFAA40B805FCF45F6E0DA2F345953FB203C52625E35B562FE8B6063E3865A151A6ED14745BC32B4EB6738ABE46E333AB5EDA3AD67E04E" +
|
|
49
|
+
"4195EE626271261D31EF6230AFD782ACC2DC0504F59707BF11592307C06402E897E53EAF18AC59A68B4A33901C6E7C9C207E4C3C3E6164BBC56B7C7E3E9FC54C" +
|
|
50
|
+
"9FEA73F5D789C04CF4FBF42DEC141B51D5C112C810DF0B4A8B9CBC93456A3E3E7DC1A9BACDC1B407E4E1688643B26D38F3FB0C5C663771DE56EF6EA0104065A7" +
|
|
51
|
+
"98F7D0BE0EC83736EC10CA7C9CAB841E051776021C4F52AA5FC1C6A056B9D80484444DA759D8DE60E6380E058F03E13B6D8104336F300BCE69052133FB26BB89" +
|
|
52
|
+
"7DB6AE877E5107E0ACF7960A6BF9C45C1DE44447B85EFAE3788455424B485EF77D4735861D2B430503EC8AB81E063C760C481A43A7B78AED1E13C643EE10EFDB" +
|
|
53
|
+
"ECFB3C83B29544EFD854514E2D11441DFB36591E7A34C1C3CA570061EA67A5169B55D055E17FD936D24076AEDC01CEB07A83D5CB2098EC6BC1729234F3825737" +
|
|
54
|
+
"628A32360C9043AEAE5C9B788E136502FD6871C1FEB031A02482B0C3B17969A7F5D2EBD082C032DC9EC7263C6D8D98C1BB22D4D00F33EC3EB9CCE1DC6A4C7736" +
|
|
55
|
+
"141CF9BF819F285F71853229907548C4B34ACED8448F142FFD4057EFAA0875D946D1D66E32551FC318FE841FFC84D5FF715E1B48C386950E280827D33883717B" +
|
|
56
|
+
"4C8063549A56B0ACCF80CA3109EFFEF3BEAF247EA6FE533FC28D4A3368D122A666AD7BEADEB643B0A1259500A33F7546141144ECD795BC92F04FA91653629760" +
|
|
57
|
+
"2A0F41F17124BEEE947F08CD6093B3855B07003FD80F28839AD1699FD1DA2EC39001A2B96B4E2A669DDAAEA6EA2AD3682F0C0C9CD28C4AEDE29E57659D0987A3" +
|
|
58
|
+
"B4C4325DC9D4322BB1E0711E644DE69071E31E40ED7DF3840EEDC87876AEC0712772BB05EA0264FBF3486BB542933FED9F1353D2F7FE2AEC1D4725DB3C9186C6" +
|
|
59
|
+
"8EF011FD237436F7A4F59E7A7E535044D447CAD3EB386DE6D971947F4AC6694B11F452EA22FE8AB036678B59E8E6802AEB650413EEECDC9E5FB1EC056A59E69F" +
|
|
60
|
+
"5E596B89BFF71ACA44F95B6A718503E42962E0706F41C4CFB2B1CCE37EA607A887E77F8493DB524B6CEC7EDDD4244810699F046074E64818F3E42CB94F2E507A" +
|
|
61
|
+
"DFD454692B8BA7F3CEFF1FF33E26013917958489B0F04C4B82919FC44BAC9DA574AF1725C9CA32D3BC898A8489CC0DAE7CA2DB9C6A7891EEEA765D4E8760F569" +
|
|
62
|
+
"1567D402CFAF483607EABF6F662D068FC49AFEF9F6908775B8F7AD0F76105A3D59B02EB3C7352CCC70562BCBE33796C52F461B8A2246C788A726329861DF8622" +
|
|
63
|
+
"8AF41C2F87A109AACCA9AED3BD00451C9A5487865287EFFF1E8FA18FC1895C351BDA2D3A2C16B2C2F156E278C16B6397C5568FC9327F2CAAAFA6A8AC20912288" +
|
|
64
|
+
"DEE4608BF94B42251AE37F9C2C19893A7E05D436CC6958C2C1328B2F9085EB7A3950A5A12792C566B0204F587E5583432B45E29CE4D812902C168356167903B3" +
|
|
65
|
+
"AD2D61181A131F37E2E19C737B80D5FD2D5187FC7BAAD71F2C7A8EAFF48DBBCD95117C720BEE6FE2B9AFDE3783DE8C8D620567B796C68D56B60DD762BAD64636" +
|
|
66
|
+
"BD8EC8E6EA2A6C1014FF6B5BFA823C46B1304346518A7D9B923E83795B555DB26C5ECE90628E5398C90D6DE52D57CDC58157BAE1E8B88F72E54F13DCEA9D7115" +
|
|
67
|
+
"10B21188D509D47F5B657F2C3B384C1168508DFB9EB059BF9480894AC51A18128953D14A1029E88C1CECB6EA46C7178B251531A8A26B43B19DE2DB0B879BB011" +
|
|
68
|
+
"040E71D2297789820A66417F1D0B48FF72BB24FDC248A19BFE7B7FCE88DB86D9853B1CB0DCA83307BF512EE30E9A00971E06C097439DD8B645C486675F00F888" +
|
|
69
|
+
"9AA4529EC7AA8A8375ECC518AECEC32F1A2BF918FFAE1AF5530BB53351A7FDE8A8E1A264B622174380CC0AD8AE3BBA40D7D9924A89DF0410EE9B182B6A77698A" +
|
|
70
|
+
"68F4F9B9A221156EE61E3B0362309B60417E259B9E8FC5521008F8C269A1211188375E793566FF1042186EED97B66B1C4E36E56D7DB4E4BF20B9E0053A69D5B8" +
|
|
71
|
+
"E3D5DCE0B9AC533E07A457AD77FF4818762AAC492A8E47756D9F676330358C390539D56F643A5BADCA0BBB82529945B193363699AF13204436D8024409399285" +
|
|
72
|
+
"FF4A4A9787A663D7C7B5B524ED0FB46F0C585214D9A67BD379BC3858A1BD3B8406D81A06FD6BA8EA4B69280437AD8299FB0E1B85BDA85D73CDDC58750ABE636C" +
|
|
73
|
+
"48E74CE4302B0460B915D8DA8681758F96D48D1C5D70857C1C677BD50867A6CE4B0A6670B7E563D45B8A82EA1067CAE2F4EF17852F2A5F8A9782F86AD63410EA" +
|
|
74
|
+
"EBC95C3CE149F846EBDEBDF6A992F1AAA6A018B03AD30F1FF36FFF31454344D3509AF7880996C1CE76CCF22C2CBAAD82778F1884C0D2079C3690834E0BA54F43" +
|
|
75
|
+
"3E04AB784FD6FB09012490DA6F3C3A610D7F694AEB2B3002B4DBE084A9ECD735BF377D8558CEA94EE480C7A8D3306748EB29AF2F746AB4A73F0F3F92AFF3CAAC" +
|
|
76
|
+
"AF4BD994C043CA810D2F48A1B027D5D2EF4B0585A3DE4D93303CF0BB4A8F30274CEBE33E64ED9A2F3BF182F0BAF4CF7F40CBB0E17FBCAA57D3C974F2FA430D22" +
|
|
77
|
+
"D0F4774E93D785701F99BFB6DE35F130A75E71F06B012D7B64F033530A3988F36B3AA66B35D22F43CD02FDB5E9BC5BAAD8A4197E0E5D94819E6F77ADD60E7493" +
|
|
78
|
+
"96E7C4185FADF519",
|
|
79
|
+
"hex",
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
export const aes128CtrCrypt = (key: Buffer, counter: Buffer, data: Buffer): Buffer => {
|
|
83
|
-
if (key.length !== 16 || counter.length !== 16)
|
|
84
|
-
throw new RangeError('AES key and counter must have 16 bytes');
|
|
83
|
+
if (key.length !== 16 || counter.length !== 16) throw new RangeError("AES key and counter must have 16 bytes");
|
|
85
84
|
const ctr = Buffer.from(counter).reverse();
|
|
86
85
|
const aes = createAesBlockCipher(Buffer.from(key).reverse());
|
|
87
86
|
const output = Buffer.alloc(data.length);
|
|
@@ -97,20 +96,16 @@ export const aes128CtrCrypt = (key: Buffer, counter: Buffer, data: Buffer): Buff
|
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
export const modcryptKey = (header: ModcryptHeader): Buffer => {
|
|
100
|
-
if (typeof header.gameCode !==
|
|
101
|
-
throw new Error(
|
|
99
|
+
if (typeof header.gameCode !== "string" || header.gameCode.length !== 4)
|
|
100
|
+
throw new Error("Expected four-character game code");
|
|
102
101
|
if (header.twlInternalFlags & 4 || (header.appflags?.[3] ?? 0) & 0x80) {
|
|
103
102
|
const key = Buffer.alloc(16);
|
|
104
|
-
key.write(header.gameTitle, 0, 12,
|
|
105
|
-
key.write(header.gameCode, 12, 4,
|
|
103
|
+
key.write(header.gameTitle, 0, 12, "ascii");
|
|
104
|
+
key.write(header.gameCode, 12, 4, "ascii");
|
|
106
105
|
return key;
|
|
107
106
|
}
|
|
108
|
-
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16)
|
|
109
|
-
|
|
110
|
-
const x = Buffer.from(
|
|
111
|
-
'Nintendo' + header.gameCode + [...header.gameCode].reverse().join(''),
|
|
112
|
-
'ascii',
|
|
113
|
-
);
|
|
107
|
+
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16) throw new Error("Missing ARM9i HMAC for modcrypt key");
|
|
108
|
+
const x = Buffer.from(`Nintendo${header.gameCode}${[...header.gameCode].reverse().join("")}`, "ascii");
|
|
114
109
|
for (let i = 0; i < 16; i++) x[i] ^= header.hmac_arm9i[i];
|
|
115
110
|
const mask = (1n << 128n) - 1n;
|
|
116
111
|
let v = x.readBigUInt64LE(0) | (x.readBigUInt64LE(8) << 64n);
|
|
@@ -123,9 +118,8 @@ export const modcryptKey = (header: ModcryptHeader): Buffer => {
|
|
|
123
118
|
};
|
|
124
119
|
|
|
125
120
|
export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
|
|
126
|
-
if (typeof gameCode !==
|
|
127
|
-
|
|
128
|
-
if (input.length < 0x800) throw new RangeError('Secure area must contain at least 0x800 bytes');
|
|
121
|
+
if (typeof gameCode !== "string" || gameCode.length !== 4) throw new Error("Expected four-character game code");
|
|
122
|
+
if (input.length < 0x800) throw new RangeError("Secure area must contain at least 0x800 bytes");
|
|
129
123
|
const output = Buffer.from(input);
|
|
130
124
|
let magic = new Uint32Array(0x412);
|
|
131
125
|
let args: [number, number, number] = [0, 0, 0];
|
|
@@ -155,7 +149,7 @@ export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
|
|
|
155
149
|
}
|
|
156
150
|
function init1() {
|
|
157
151
|
magic = Uint32Array.from({ length: 0x412 }, (_, i) => KEY1.readUInt32LE(i * 4));
|
|
158
|
-
const code = Buffer.from(gameCode,
|
|
152
|
+
const code = Buffer.from(gameCode, "ascii").readUInt32LE();
|
|
159
153
|
args = [code, code >>> 1, (code << 1) >>> 0];
|
|
160
154
|
init2();
|
|
161
155
|
init2();
|
package/src/nitro/filesystem.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Based on NitroHelper FNT/FAT/OverlayTable, GPL-3.0.
|
|
2
|
-
import { bytes, range } from
|
|
2
|
+
import { bytes, range } from "./binary";
|
|
3
3
|
|
|
4
4
|
/** A file in the original ROM, optionally replaced with new bytes. */
|
|
5
5
|
export interface NitroFile {
|
|
@@ -29,8 +29,8 @@ export class FileAllocationTable {
|
|
|
29
29
|
|
|
30
30
|
constructor(input: Uint8Array, offset: number, size: number) {
|
|
31
31
|
const data = bytes(input);
|
|
32
|
-
const table = range(data, offset, size,
|
|
33
|
-
if (size % 8 || size / 8 > 65535) throw new Error(
|
|
32
|
+
const table = range(data, offset, size, "FAT");
|
|
33
|
+
if (size % 8 || size / 8 > 65535) throw new Error("Invalid FAT entry count");
|
|
34
34
|
this.entries = [];
|
|
35
35
|
for (let p = 0; p < size; p += 8) {
|
|
36
36
|
const start = table.readUInt32LE(p),
|
|
@@ -38,9 +38,7 @@ export class FileAllocationTable {
|
|
|
38
38
|
range(data, start, end - start, `FAT entry ${p / 8}`);
|
|
39
39
|
this.entries.push({ id: p / 8, offset: start, size: end - start });
|
|
40
40
|
}
|
|
41
|
-
this.sortedIDs = [...this.entries]
|
|
42
|
-
.sort((a, b) => a.offset - b.offset || a.id - b.id)
|
|
43
|
-
.map((entry) => entry.id);
|
|
41
|
+
this.sortedIDs = [...this.entries].sort((a, b) => a.offset - b.offset || a.id - b.id).map((entry) => entry.id);
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -48,21 +46,20 @@ export class FileNameTable {
|
|
|
48
46
|
readonly root: NitroFolder;
|
|
49
47
|
|
|
50
48
|
constructor(fat: FileAllocationTable, input: Uint8Array, offset: number, size: number) {
|
|
51
|
-
const table = range(bytes(input), offset, size,
|
|
52
|
-
range(table, 0, 8,
|
|
49
|
+
const table = range(bytes(input), offset, size, "FNT");
|
|
50
|
+
range(table, 0, 8, "FNT root directory");
|
|
53
51
|
const count = table.readUInt16LE(6);
|
|
54
|
-
if (count < 1 || count > 4096) throw new Error(
|
|
55
|
-
range(table, 0, count * 8,
|
|
56
|
-
const decoder = new TextDecoder(
|
|
52
|
+
if (count < 1 || count > 4096) throw new Error("Invalid FNT directory count");
|
|
53
|
+
range(table, 0, count * 8, "FNT directory table");
|
|
54
|
+
const decoder = new TextDecoder("shift_jis");
|
|
57
55
|
const visited = new Set<number>();
|
|
58
56
|
const fileIds = new Set<number>();
|
|
59
57
|
const parse = (index: number, name: string, depth: number): NitroFolder => {
|
|
60
|
-
if (depth > 256 || index >= count || visited.has(index))
|
|
61
|
-
throw new Error('Invalid FNT directory tree or cycle');
|
|
58
|
+
if (depth > 256 || index >= count || visited.has(index)) throw new Error("Invalid FNT directory tree or cycle");
|
|
62
59
|
visited.add(index);
|
|
63
60
|
let cursor = table.readUInt32LE(index * 8);
|
|
64
61
|
let fileId = table.readUInt16LE(index * 8 + 4);
|
|
65
|
-
if (cursor < count * 8) throw new Error(
|
|
62
|
+
if (cursor < count * 8) throw new Error("FNT directory names overlap directory table");
|
|
66
63
|
const folder: NitroFolder = {
|
|
67
64
|
name,
|
|
68
65
|
id: index === 0 ? 0xffff : 0xf000 + index,
|
|
@@ -71,31 +68,31 @@ export class FileNameTable {
|
|
|
71
68
|
};
|
|
72
69
|
const names = new Set<string>();
|
|
73
70
|
while (true) {
|
|
74
|
-
const tag = range(table, cursor++, 1,
|
|
71
|
+
const tag = range(table, cursor++, 1, "FNT name")[0];
|
|
75
72
|
if (!tag) break;
|
|
76
73
|
const length = tag & 0x7f;
|
|
77
|
-
if (!length) throw new Error(
|
|
78
|
-
const filename = decoder.decode(range(table, cursor, length,
|
|
74
|
+
if (!length) throw new Error("Empty FNT directory name");
|
|
75
|
+
const filename = decoder.decode(range(table, cursor, length, "FNT filename"));
|
|
79
76
|
cursor += length;
|
|
80
77
|
if (
|
|
81
|
-
filename.includes(
|
|
82
|
-
filename.includes(
|
|
83
|
-
filename.includes(
|
|
84
|
-
filename ===
|
|
85
|
-
filename ===
|
|
78
|
+
filename.includes("/") ||
|
|
79
|
+
filename.includes("\\") ||
|
|
80
|
+
filename.includes("\0") ||
|
|
81
|
+
filename === "." ||
|
|
82
|
+
filename === ".."
|
|
86
83
|
)
|
|
87
|
-
throw new Error(
|
|
84
|
+
throw new Error("Invalid FNT path component");
|
|
88
85
|
const normalized = filename.toLowerCase();
|
|
89
86
|
if (names.has(normalized)) throw new Error(`Duplicate FNT path: ${filename}`);
|
|
90
87
|
names.add(normalized);
|
|
91
88
|
if (tag & 0x80) {
|
|
92
|
-
const id = range(table, cursor, 2,
|
|
89
|
+
const id = range(table, cursor, 2, "FNT directory id").readUInt16LE();
|
|
93
90
|
cursor += 2;
|
|
94
|
-
if (id < 0xf000) throw new Error(
|
|
91
|
+
if (id < 0xf000) throw new Error("Invalid FNT directory id");
|
|
95
92
|
folder.folders.push(parse(id & 0xfff, filename, depth + 1));
|
|
96
93
|
} else {
|
|
97
94
|
const entry = fat.entries[fileId++];
|
|
98
|
-
if (!entry) throw new Error(
|
|
95
|
+
if (!entry) throw new Error("FNT file id is outside FAT");
|
|
99
96
|
if (fileIds.has(entry.id)) throw new Error(`Duplicate FNT file id: ${entry.id}`);
|
|
100
97
|
fileIds.add(entry.id);
|
|
101
98
|
folder.files.push({ name: filename, ...entry });
|
|
@@ -103,7 +100,7 @@ export class FileNameTable {
|
|
|
103
100
|
}
|
|
104
101
|
return folder;
|
|
105
102
|
};
|
|
106
|
-
this.root = { name:
|
|
103
|
+
this.root = { name: "root", id: 0xffff, files: [], folders: [parse(0, "data", 0)] };
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
static findFile(id: number, folder: NitroFolder): NitroFile | undefined {
|
|
@@ -128,14 +125,14 @@ export interface OverlayItem {
|
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
const overlayFields = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
"overlayId",
|
|
129
|
+
"ramAddress",
|
|
130
|
+
"ramSize",
|
|
131
|
+
"bssSize",
|
|
132
|
+
"staticInitialiserStartAddress",
|
|
133
|
+
"staticInitialiserEndAddress",
|
|
134
|
+
"fileId",
|
|
135
|
+
"reserved",
|
|
139
136
|
] as const;
|
|
140
137
|
|
|
141
138
|
export class OverlayTable {
|
|
@@ -147,8 +144,8 @@ export class OverlayTable {
|
|
|
147
144
|
readonly isArm9: boolean,
|
|
148
145
|
) {
|
|
149
146
|
if (!size) return;
|
|
150
|
-
const table = range(bytes(input), offset, size,
|
|
151
|
-
if (size % 32) throw new Error(
|
|
147
|
+
const table = range(bytes(input), offset, size, "overlay table");
|
|
148
|
+
if (size % 32) throw new Error("Invalid overlay table size");
|
|
152
149
|
for (let p = 0; p < size; p += 32) {
|
|
153
150
|
if (table.readUInt32LE(p) === 0xffffffff) break;
|
|
154
151
|
this.entries.push({
|
|
@@ -166,10 +163,10 @@ export class OverlayTable {
|
|
|
166
163
|
readBasicOverlays(fat: FileAllocationTable): NitroFile[] {
|
|
167
164
|
return this.entries.map((item) => {
|
|
168
165
|
const file = fat.entries[item.fileId];
|
|
169
|
-
if (!file) throw new Error(
|
|
166
|
+
if (!file) throw new Error("Overlay file id is outside FAT");
|
|
170
167
|
return {
|
|
171
168
|
...file,
|
|
172
|
-
name: `overlay${this.isArm9 ?
|
|
169
|
+
name: `overlay${this.isArm9 ? "" : "7"}_${String(item.overlayId).padStart(4, "0")}.bin`,
|
|
173
170
|
};
|
|
174
171
|
});
|
|
175
172
|
}
|