@wasm-gaming/mgba-wasm 0.1.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.
- package/README.md +203 -0
- package/dist/manifest.json +146 -0
- package/dist/mgba/mgba.config.d.ts +41 -0
- package/dist/mgba/mgba.config.d.ts.map +1 -0
- package/dist/mgba/mgba.config.js +93 -0
- package/dist/mgba/mgba.config.js.map +1 -0
- package/dist/mgba/mgba.js +2 -0
- package/dist/mgba/mgba.manifest.d.ts +4 -0
- package/dist/mgba/mgba.manifest.d.ts.map +1 -0
- package/dist/mgba/mgba.manifest.js +38 -0
- package/dist/mgba/mgba.manifest.js.map +1 -0
- package/dist/mgba/mgba.options.d.ts +171 -0
- package/dist/mgba/mgba.options.d.ts.map +1 -0
- package/dist/mgba/mgba.options.js +273 -0
- package/dist/mgba/mgba.options.js.map +1 -0
- package/dist/mgba/mgba.sdk.d.ts +24 -0
- package/dist/mgba/mgba.sdk.d.ts.map +1 -0
- package/dist/mgba/mgba.sdk.js +846 -0
- package/dist/mgba/mgba.sdk.js.map +1 -0
- package/dist/mgba/mgba.wasm +0 -0
- package/dist/mgba/mgba.zip.d.ts +36 -0
- package/dist/mgba/mgba.zip.d.ts.map +1 -0
- package/dist/mgba/mgba.zip.js +105 -0
- package/dist/mgba/mgba.zip.js.map +1 -0
- package/package.json +53 -0
- package/src/mgba.config.ts +145 -0
- package/src/mgba.manifest.ts +43 -0
- package/src/mgba.options.ts +456 -0
- package/src/mgba.sdk.ts +967 -0
- package/src/mgba.zip.ts +140 -0
package/src/mgba.zip.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A zip reader just large enough to pull a ROM out of an archive.
|
|
3
|
+
*
|
|
4
|
+
* mGBA can read zips when it is built against libzip or minizip, but linking
|
|
5
|
+
* either into an Emscripten build means vendoring a C dependency for something
|
|
6
|
+
* the platform already does: `DecompressionStream('deflate-raw')` is in every
|
|
7
|
+
* browser this SDK targets and in Node 18+. So the container is parsed here —
|
|
8
|
+
* end-of-central-directory, then the central directory — and the one entry that
|
|
9
|
+
* looks like a ROM is inflated.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately partial: no encryption, no zip64, no multi-disk. Those do not
|
|
12
|
+
* show up around cartridge dumps, and failing loudly beats half-supporting them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SIG_EOCD = 0x06054b50;
|
|
16
|
+
const SIG_CENTRAL = 0x02014b50;
|
|
17
|
+
const SIG_LOCAL = 0x04034b50;
|
|
18
|
+
|
|
19
|
+
const STORED = 0;
|
|
20
|
+
const DEFLATED = 8;
|
|
21
|
+
|
|
22
|
+
export interface ZipEntry {
|
|
23
|
+
name: string;
|
|
24
|
+
compression: number;
|
|
25
|
+
compressedSize: number;
|
|
26
|
+
uncompressedSize: number;
|
|
27
|
+
localHeaderOffset: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A zip always starts with a local file header signature. */
|
|
31
|
+
export function isZip(bytes: Uint8Array): boolean {
|
|
32
|
+
return (
|
|
33
|
+
bytes.length > 4 &&
|
|
34
|
+
bytes[0] === 0x50 &&
|
|
35
|
+
bytes[1] === 0x4b &&
|
|
36
|
+
bytes[2] === 0x03 &&
|
|
37
|
+
bytes[3] === 0x04
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findEocd(view: DataView): number {
|
|
42
|
+
// The EOCD is last, but a trailing comment of up to 64 KiB may follow it.
|
|
43
|
+
const min = Math.max(0, view.byteLength - 0x10000 - 22);
|
|
44
|
+
for (let i = view.byteLength - 22; i >= min; i--) {
|
|
45
|
+
if (view.getUint32(i, true) === SIG_EOCD) return i;
|
|
46
|
+
}
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function listEntries(bytes: Uint8Array): ZipEntry[] {
|
|
51
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
52
|
+
const eocd = findEocd(view);
|
|
53
|
+
if (eocd < 0) throw new Error('mgba: not a zip archive (no end-of-central-directory record)');
|
|
54
|
+
|
|
55
|
+
const count = view.getUint16(eocd + 10, true);
|
|
56
|
+
let offset = view.getUint32(eocd + 16, true);
|
|
57
|
+
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
const entries: ZipEntry[] = [];
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < count; i++) {
|
|
62
|
+
if (view.getUint32(offset, true) !== SIG_CENTRAL) break;
|
|
63
|
+
|
|
64
|
+
const nameLength = view.getUint16(offset + 28, true);
|
|
65
|
+
const extraLength = view.getUint16(offset + 30, true);
|
|
66
|
+
const commentLength = view.getUint16(offset + 32, true);
|
|
67
|
+
|
|
68
|
+
entries.push({
|
|
69
|
+
name: decoder.decode(bytes.subarray(offset + 46, offset + 46 + nameLength)),
|
|
70
|
+
compression: view.getUint16(offset + 10, true),
|
|
71
|
+
compressedSize: view.getUint32(offset + 20, true),
|
|
72
|
+
uncompressedSize: view.getUint32(offset + 24, true),
|
|
73
|
+
localHeaderOffset: view.getUint32(offset + 42, true),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readEntry(bytes: Uint8Array, entry: ZipEntry): Promise<Uint8Array> {
|
|
83
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
84
|
+
const header = entry.localHeaderOffset;
|
|
85
|
+
if (view.getUint32(header, true) !== SIG_LOCAL) {
|
|
86
|
+
throw new Error(`mgba: corrupt zip entry "${entry.name}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The local header repeats the name and extra field with its own lengths,
|
|
90
|
+
// which are the ones that count for locating the data.
|
|
91
|
+
const nameLength = view.getUint16(header + 26, true);
|
|
92
|
+
const extraLength = view.getUint16(header + 28, true);
|
|
93
|
+
const start = header + 30 + nameLength + extraLength;
|
|
94
|
+
const data = bytes.subarray(start, start + entry.compressedSize);
|
|
95
|
+
|
|
96
|
+
if (entry.compression === STORED) return data.slice();
|
|
97
|
+
if (entry.compression !== DEFLATED) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`mgba: zip entry "${entry.name}" uses an unsupported compression method (${entry.compression})`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (typeof DecompressionStream === 'undefined') {
|
|
103
|
+
throw new Error('mgba: this environment cannot inflate zip entries (no DecompressionStream)');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stream = new Blob([data as BlobPart])
|
|
107
|
+
.stream()
|
|
108
|
+
.pipeThrough(new DecompressionStream('deflate-raw'));
|
|
109
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the ROM inside a zip, or the input unchanged when it is not one.
|
|
114
|
+
*
|
|
115
|
+
* Cartridge archives are almost always a single ROM plus the odd text file, so
|
|
116
|
+
* the pick is "the first entry with an extension mGBA knows"; if none of them
|
|
117
|
+
* carries a useful extension, the largest entry wins, which is what a ROM is.
|
|
118
|
+
*/
|
|
119
|
+
export async function extractRom(
|
|
120
|
+
bytes: Uint8Array,
|
|
121
|
+
accept: readonly string[],
|
|
122
|
+
): Promise<{ bytes: Uint8Array; name?: string }> {
|
|
123
|
+
if (!isZip(bytes)) return { bytes };
|
|
124
|
+
|
|
125
|
+
const entries = listEntries(bytes).filter(
|
|
126
|
+
(entry) => !entry.name.endsWith('/') && entry.uncompressedSize > 0,
|
|
127
|
+
);
|
|
128
|
+
if (!entries.length) throw new Error('mgba: the zip archive contains no files');
|
|
129
|
+
|
|
130
|
+
const byExtension = entries.find((entry) =>
|
|
131
|
+
accept.some((ext) => entry.name.toLowerCase().endsWith(ext)),
|
|
132
|
+
);
|
|
133
|
+
const chosen =
|
|
134
|
+
byExtension ??
|
|
135
|
+
entries.reduce((largest, entry) =>
|
|
136
|
+
entry.uncompressedSize > largest.uncompressedSize ? entry : largest,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return { bytes: await readEntry(bytes, chosen), name: chosen.name };
|
|
140
|
+
}
|