@wasm-gaming/fbneo-wasm 0.1.0 → 0.1.2

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.
Files changed (43) hide show
  1. package/README.md +78 -1
  2. package/data/romsets/atari.json +12183 -0
  3. package/data/romsets/capcom.json +66781 -0
  4. package/data/romsets/cave.json +4888 -0
  5. package/data/romsets/channelf.json +463 -0
  6. package/data/romsets/coleco.json +5921 -0
  7. package/data/romsets/cps3.json +1777 -0
  8. package/data/romsets/dataeast.json +22415 -0
  9. package/data/romsets/galaxian.json +14107 -0
  10. package/data/romsets/index.json +116 -0
  11. package/data/romsets/irem.json +15032 -0
  12. package/data/romsets/konami.json +24780 -0
  13. package/data/romsets/megadrive.json +26352 -0
  14. package/data/romsets/midway.json +13213 -0
  15. package/data/romsets/msx.json +16287 -0
  16. package/data/romsets/neogeo.json +39274 -0
  17. package/data/romsets/nes.json +28301 -0
  18. package/data/romsets/pce.json +4397 -0
  19. package/data/romsets/pgm.json +8860 -0
  20. package/data/romsets/pgm2.json +397 -0
  21. package/data/romsets/pre90s.json +143783 -0
  22. package/data/romsets/psikyo.json +2205 -0
  23. package/data/romsets/pst90s.json +84614 -0
  24. package/data/romsets/sega.json +54871 -0
  25. package/data/romsets/sg1000.json +2300 -0
  26. package/data/romsets/sms.json +13532 -0
  27. package/data/romsets/snes.json +23630 -0
  28. package/data/romsets/spectrum.json +30187 -0
  29. package/data/romsets/taito.json +40373 -0
  30. package/data/romsets/toaplan.json +8177 -0
  31. package/dist/fbneo/neo.d.ts +83 -0
  32. package/dist/fbneo/neo.d.ts.map +1 -0
  33. package/dist/fbneo/neo.js +357 -0
  34. package/dist/fbneo/neo.js.map +1 -0
  35. package/dist/fbneo/romset.d.ts +0 -0
  36. package/dist/fbneo/romset.d.ts.map +1 -0
  37. package/dist/fbneo/romset.js +0 -0
  38. package/dist/fbneo/romset.js.map +1 -0
  39. package/package.json +17 -1
  40. package/src/neo.ts +463 -0
  41. package/src/romset.ts +0 -0
  42. package/dist/fbneo/fbneo.js +0 -2
  43. package/dist/fbneo/fbneo.wasm +0 -0
package/src/neo.ts ADDED
@@ -0,0 +1,463 @@
1
+ // Opt-in helper: turn a Neo Geo `.neo` file (the NeoSD / Terraonion flash-cart
2
+ // image) into the FBNeo ROM-set zip the core can actually boot — in the browser,
3
+ // with no dependencies.
4
+ //
5
+ // FBNeo — like MAME — only reads `<driver>.zip` / `<driver>.7z` holding the
6
+ // individual *chip* dumps (p1, s1, c1..cN, m1, v1..vN). A `.neo` is the opposite
7
+ // packaging: a 4096-byte header followed by each region as one contiguous blob,
8
+ // with the C (sprite) region byte-interleaved the way the cartridge wires it. So
9
+ // a `.neo` can never be handed to the engine directly — but every byte FBNeo
10
+ // wants is in there, and this module rebuilds the chip split:
11
+ //
12
+ // header: "NEO1" | P size | S size | M size | V1 size | V2 size | C size | ...
13
+ // data: P | S | M | V1 | V2 | C (C = even bytes -> c1, odd -> c2)
14
+ //
15
+ // The chip layout (how many chips per region and how big each one is) comes from
16
+ // the same `data/romsets/neogeo.json` dataset the `romset` helper uses for
17
+ // content identity: for systems that need it, `gen-romset-index.mjs` records each
18
+ // ROM's size (`s`) and region (`r`) alongside its CRC-32. The driver is
19
+ // identified by matching the `.neo` header's region sizes against those layouts,
20
+ // and every carved chip is CRC-32 verified before a zip is written — if a set
21
+ // needs a layout this simple carve cannot reproduce (some SMA-protected games
22
+ // store their program ROM inside the custom chip), this throws rather than hand
23
+ // the engine a silently broken zip.
24
+ //
25
+ // @example
26
+ // const neogeo = await fetch('/romsets/neogeo.json').then((r) => r.json());
27
+ // if (isNeoFile(bytes)) {
28
+ // const { driver, zip } = neoToRomset(bytes, neogeo);
29
+ // await engine.load({ ...config, assets: { ...assets, rom: zip }, options: { driver } });
30
+ // }
31
+
32
+ import type { RomsetGame, RomsetSystem } from './romset.js';
33
+
34
+ /** A Neo Geo cartridge region, in the order a `.neo` file stores them. */
35
+ export type NeoRegion = 'P' | 'S' | 'M' | 'V1' | 'V2' | 'C';
36
+
37
+ /** The regions in `.neo` file order — the order the blobs follow the header. */
38
+ export const NEO_REGIONS: readonly NeoRegion[] = ['P', 'S', 'M', 'V1', 'V2', 'C'];
39
+
40
+ /** Header fields of a `.neo` file. */
41
+ export interface NeoInfo {
42
+ /** Cartridge title from the header's `Name[33]` field. */
43
+ name: string;
44
+ /** Manufacturer from the header's `Manu[17]` field. */
45
+ manufacturer: string;
46
+ year: number;
47
+ /** Bytes each region occupies in the file. */
48
+ sizes: Record<NeoRegion, number>;
49
+ }
50
+
51
+ /** A parsed `.neo` file: its header plus a view over each region's bytes. */
52
+ export interface NeoFile extends NeoInfo {
53
+ /** Views into the source buffer — no copying. */
54
+ regions: Record<NeoRegion, Uint8Array>;
55
+ }
56
+
57
+ /** One chip carved out of a `.neo`, ready to become a zip entry. */
58
+ export interface NeoChip {
59
+ /** Internal ROM name FBNeo expects (e.g. `201-c1.c1`). */
60
+ name: string;
61
+ /** Lowercase 8-hex CRC-32 of `data`, verified against the driver table. */
62
+ crc: string;
63
+ region: NeoRegion;
64
+ data: Uint8Array;
65
+ }
66
+
67
+ /** The result of a successful conversion. */
68
+ export interface NeoRomset {
69
+ /** FBNeo driver short name — pass as `options.driver`. */
70
+ driver: string;
71
+ system: string;
72
+ /** Clone parent's driver name, or `null` for a parent set. */
73
+ parent: string | null;
74
+ info: NeoInfo;
75
+ chips: NeoChip[];
76
+ /**
77
+ * The rebuilt ROM set as a *stored* (uncompressed) zip — feed as `assets.rom`.
78
+ * Deflating would only be undone by FBNeo on the next line, so the bytes are
79
+ * stored: the conversion stays synchronous and costs no extra CPU.
80
+ */
81
+ zip: Uint8Array;
82
+ }
83
+
84
+ export interface NeoToRomsetOptions {
85
+ /** Force a driver instead of matching the header's region sizes against every set. */
86
+ driver?: string;
87
+ }
88
+
89
+ const NEO_HEADER_SIZE = 4096;
90
+ const NAME_OFFSET = 0x2c;
91
+ const NAME_LENGTH = 33;
92
+ const MANUFACTURER_OFFSET = 0x4d;
93
+ const MANUFACTURER_LENGTH = 17;
94
+
95
+ /** Cheap sniff: does this look like a `.neo` flash-cart image? */
96
+ export function isNeoFile(bytes: Uint8Array): boolean {
97
+ return (
98
+ bytes.length > NEO_HEADER_SIZE &&
99
+ bytes[0] === 0x4e /* N */ &&
100
+ bytes[1] === 0x45 /* E */ &&
101
+ bytes[2] === 0x4f /* O */
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Read a `.neo` file's header and slice out each region.
107
+ *
108
+ * @throws if the magic is missing or the file is shorter than its header describes.
109
+ */
110
+ export function parseNeo(bytes: Uint8Array): NeoFile {
111
+ if (!isNeoFile(bytes)) throw new Error('neo: not a .neo file (missing NEO magic)');
112
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
113
+
114
+ // Region sizes are six consecutive LE uint32s, but the header stores them in
115
+ // P S M V1 V2 C order while the C blob is written last — same order here.
116
+ const sizes = {
117
+ P: view.getUint32(4, true),
118
+ S: view.getUint32(8, true),
119
+ M: view.getUint32(12, true),
120
+ V1: view.getUint32(16, true),
121
+ V2: view.getUint32(20, true),
122
+ C: view.getUint32(24, true),
123
+ } satisfies Record<NeoRegion, number>;
124
+
125
+ const regions = {} as Record<NeoRegion, Uint8Array>;
126
+ let offset = NEO_HEADER_SIZE;
127
+ for (const region of NEO_REGIONS) {
128
+ const end = offset + sizes[region];
129
+ if (end > bytes.length) {
130
+ throw new Error(
131
+ `neo: truncated file — header describes ${end} bytes through region ${region}, ` +
132
+ `file is ${bytes.length}`,
133
+ );
134
+ }
135
+ regions[region] = bytes.subarray(offset, end);
136
+ offset = end;
137
+ }
138
+
139
+ return {
140
+ name: cString(bytes, NAME_OFFSET, NAME_LENGTH),
141
+ manufacturer: cString(bytes, MANUFACTURER_OFFSET, MANUFACTURER_LENGTH),
142
+ year: view.getUint32(28, true),
143
+ sizes,
144
+ regions,
145
+ };
146
+ }
147
+
148
+ /** Read a fixed-width NUL-padded latin-1 string field. */
149
+ function cString(bytes: Uint8Array, start: number, length: number): string {
150
+ let end = start;
151
+ const limit = Math.min(start + length, bytes.length);
152
+ while (end < limit && bytes[end] !== 0) end++;
153
+ let out = '';
154
+ for (let i = start; i < end; i++) out += String.fromCharCode(bytes[i]!);
155
+ return out;
156
+ }
157
+
158
+ /**
159
+ * Rebuild an FBNeo ROM set from a `.neo` file.
160
+ *
161
+ * @param bytes The raw `.neo` bytes.
162
+ * @param datasets One or more romset datasets carrying chip layouts (`s`/`r` per
163
+ * ROM) — in practice `data/romsets/neogeo.json`.
164
+ * @throws if no driver's chip layout matches the header's region sizes, or if
165
+ * every size-matched candidate fails CRC-32 verification.
166
+ */
167
+ export function neoToRomset(
168
+ bytes: Uint8Array,
169
+ datasets: RomsetSystem | readonly RomsetSystem[],
170
+ options: NeoToRomsetOptions = {},
171
+ ): NeoRomset {
172
+ const neo = parseNeo(bytes);
173
+ const systems: readonly RomsetSystem[] = Array.isArray(datasets)
174
+ ? (datasets as readonly RomsetSystem[])
175
+ : [datasets as RomsetSystem];
176
+
177
+ // Candidates: drivers whose chip layout adds up to exactly this .neo's regions.
178
+ // A size match alone can be ambiguous between a set and its clones, so this is
179
+ // only a filter — the CRC check below decides. Parents are tried first so the
180
+ // canonical boot name wins when a clone is byte-identical.
181
+ const candidates: { system: string; driver: string; game: RomsetGame }[] = [];
182
+ for (const system of systems) {
183
+ for (const [driver, game] of Object.entries(system.games)) {
184
+ if (options.driver ? driver !== options.driver : !layoutMatchesSizes(game, neo.sizes)) continue;
185
+ candidates.push({ system: system.system, driver, game });
186
+ }
187
+ }
188
+ candidates.sort(
189
+ (a, b) =>
190
+ Number(a.game.parent === null) - Number(b.game.parent === null) ||
191
+ a.driver.localeCompare(b.driver),
192
+ );
193
+
194
+ if (!candidates.length) {
195
+ throw new Error(
196
+ options.driver
197
+ ? `neo: driver "${options.driver}" is not in the supplied dataset(s)`
198
+ : `neo: no driver matches this file's region sizes ` +
199
+ `(${NEO_REGIONS.map((r) => `${r}=0x${neo.sizes[r].toString(16)}`).join(' ')}) — ` +
200
+ 'not a Neo Geo set this FBNeo build knows',
201
+ );
202
+ }
203
+
204
+ const failures: string[] = [];
205
+ for (const { system, driver, game } of candidates) {
206
+ let carved: CarvedChip[];
207
+ try {
208
+ carved = carve(game, neo.regions);
209
+ } catch (err) {
210
+ failures.push(`${driver}: ${(err as Error).message}`);
211
+ continue;
212
+ }
213
+ // Every chip must hash to the CRC-32 the driver table declares. This is what
214
+ // separates a real match from a set that merely happens to share region sizes,
215
+ // and what catches layouts this straight carve cannot reproduce.
216
+ const chips: NeoChip[] = carved.map((chip) => ({
217
+ name: chip.name,
218
+ region: chip.region,
219
+ data: chip.data,
220
+ crc: crc32Hex(chip.data),
221
+ }));
222
+ const bad = chips.filter((chip, i) => chip.crc !== carved[i]!.expected);
223
+ if (bad.length) {
224
+ failures.push(
225
+ `${driver}: ${bad.length}/${chips.length} chips CRC-mismatched ` +
226
+ `(${bad.slice(0, 4).map((c) => c.name).join(', ')}${bad.length > 4 ? ', …' : ''})`,
227
+ );
228
+ continue;
229
+ }
230
+ return {
231
+ driver,
232
+ system,
233
+ parent: game.parent,
234
+ info: { name: neo.name, manufacturer: neo.manufacturer, year: neo.year, sizes: neo.sizes },
235
+ chips,
236
+ zip: zipStore(chips),
237
+ };
238
+ }
239
+
240
+ throw new Error(
241
+ `neo: matched ${candidates.length} driver(s) by size but none by content:\n ${failures.join('\n ')}`,
242
+ );
243
+ }
244
+
245
+ /** The chips of a game's layout, in table order. Sets without layout data yield none. */
246
+ function layoutChips(game: RomsetGame): { n: string; c: string; s: number; r: NeoRegion }[] {
247
+ const out: { n: string; c: string; s: number; r: NeoRegion }[] = [];
248
+ for (const rom of game.roms) {
249
+ // Rows outside the six cartridge regions (BIOS, the SMA custom chip, padding)
250
+ // carry no `r` and occupy no bytes in a .neo — skip without moving any cursor.
251
+ if (rom.r === undefined || rom.s === undefined) continue;
252
+ if (!NEO_REGIONS.includes(rom.r as NeoRegion)) continue;
253
+ out.push({ n: rom.n, c: rom.c, s: rom.s, r: rom.r as NeoRegion });
254
+ }
255
+ return out;
256
+ }
257
+
258
+ function layoutMatchesSizes(game: RomsetGame, sizes: Record<NeoRegion, number>): boolean {
259
+ const chips = layoutChips(game);
260
+ if (!chips.length) return false;
261
+ const sums: Record<NeoRegion, number> = { P: 0, S: 0, M: 0, V1: 0, V2: 0, C: 0 };
262
+ for (const chip of chips) sums[chip.r] += chip.s;
263
+ return NEO_REGIONS.every((r) => sums[r] === sizes[r]);
264
+ }
265
+
266
+ /** A chip sliced out of the blobs, carrying the CRC-32 its driver table expects. */
267
+ interface CarvedChip {
268
+ name: string;
269
+ region: NeoRegion;
270
+ data: Uint8Array;
271
+ /** CRC-32 from the driver table — what `data` has to hash to. */
272
+ expected: string;
273
+ }
274
+
275
+ /**
276
+ * Carve a driver's chip files out of the `.neo` regions.
277
+ *
278
+ * C chips come in even/odd pairs sharing one interleaved slice of the C region;
279
+ * every other region is a straight sequential split.
280
+ */
281
+ function carve(game: RomsetGame, regions: Record<NeoRegion, Uint8Array>): CarvedChip[] {
282
+ const layout = layoutChips(game);
283
+ const cursor: Record<NeoRegion, number> = { P: 0, S: 0, M: 0, V1: 0, V2: 0, C: 0 };
284
+ const out: CarvedChip[] = [];
285
+
286
+ // Size matching normally guarantees the cursors land exactly on each region's
287
+ // end, but a forced `options.driver` can ask for a layout that overruns — and
288
+ // `subarray` would clamp rather than complain, leaving a short chip to fail CRC
289
+ // for a misleading reason.
290
+ const take = (region: NeoRegion, length: number): Uint8Array => {
291
+ const start = cursor[region];
292
+ if (start + length > regions[region].length) {
293
+ throw new Error(
294
+ `region ${region} is ${regions[region].length} bytes, layout needs at least ${start + length}`,
295
+ );
296
+ }
297
+ cursor[region] = start + length;
298
+ return regions[region].subarray(start, start + length);
299
+ };
300
+
301
+ for (let i = 0; i < layout.length; i++) {
302
+ const rom = layout[i]!;
303
+
304
+ if (rom.r !== 'C') {
305
+ const first = cursor[rom.r] === 0;
306
+ let data = take(rom.r, rom.s);
307
+ if (game.swapP && rom.r === 'P' && first) {
308
+ // On these carts the first program ROM has its two halves swapped; FBNeo
309
+ // undoes that after loading (neogeo.cpp, HARDWARE_SNK_SWAPP), so a .neo —
310
+ // which stores the cart's own order — has to be swapped back to match the
311
+ // dump's CRC.
312
+ const half = rom.s >> 1;
313
+ const swapped = new Uint8Array(rom.s);
314
+ swapped.set(data.subarray(half), 0);
315
+ swapped.set(data.subarray(0, half), rom.s - half);
316
+ data = swapped;
317
+ }
318
+ out.push({ name: rom.n, expected: rom.c, region: rom.r, data });
319
+ continue;
320
+ }
321
+
322
+ // Interleaved pair: this chip supplies the even bytes, the next one the odd.
323
+ const mate = layout[i + 1];
324
+ if (!mate || mate.r !== 'C' || mate.s !== rom.s) {
325
+ throw new Error(`unpaired C ROM "${rom.n}" — cannot de-interleave sprite data`);
326
+ }
327
+ const slice = take('C', rom.s * 2);
328
+ const even = new Uint8Array(rom.s);
329
+ const odd = new Uint8Array(rom.s);
330
+ for (let b = 0; b < rom.s; b++) {
331
+ even[b] = slice[2 * b]!;
332
+ odd[b] = slice[2 * b + 1]!;
333
+ }
334
+ out.push(
335
+ { name: rom.n, expected: rom.c, region: 'C', data: even },
336
+ { name: mate.n, expected: mate.c, region: 'C', data: odd },
337
+ );
338
+ i++;
339
+ }
340
+
341
+ return out;
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // CRC-32 and a minimal stored-zip writer
346
+ // ---------------------------------------------------------------------------
347
+
348
+ let crcTable: Uint32Array | null = null;
349
+
350
+ function getCrcTable(): Uint32Array {
351
+ if (crcTable) return crcTable;
352
+ const table = new Uint32Array(256);
353
+ for (let i = 0; i < 256; i++) {
354
+ let c = i;
355
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
356
+ table[i] = c >>> 0;
357
+ }
358
+ return (crcTable = table);
359
+ }
360
+
361
+ /** CRC-32 (IEEE, the one zip and FBNeo's ROM tables use) of `bytes`. */
362
+ export function crc32(bytes: Uint8Array): number {
363
+ const table = getCrcTable();
364
+ let c = 0xffffffff;
365
+ for (let i = 0; i < bytes.length; i++) c = table[(c ^ bytes[i]!) & 0xff]! ^ (c >>> 8);
366
+ return (c ^ 0xffffffff) >>> 0;
367
+ }
368
+
369
+ /** Lowercase 8-hex CRC-32, the form the romset dataset stores. */
370
+ export function crc32Hex(bytes: Uint8Array): string {
371
+ return crc32(bytes).toString(16).padStart(8, '0');
372
+ }
373
+
374
+ const LFH_SIG = 0x04034b50;
375
+ const CDH_SIG = 0x02014b50;
376
+ const EOCD_SIG = 0x06054b50;
377
+ const DOS_EPOCH_DATE = 0x0021; // 1980-01-01, the earliest date the format can express
378
+
379
+ /**
380
+ * Build a zip with every entry *stored* (method 0).
381
+ *
382
+ * FBNeo decompresses each chip straight into its ROM buffer, so deflating here
383
+ * would just be undone a moment later; storing keeps this synchronous (no
384
+ * `CompressionStream`) and costs one pass over the data.
385
+ */
386
+ export function zipStore(entries: readonly { name: string; data: Uint8Array }[]): Uint8Array {
387
+ const names = entries.map((e) => latin1(e.name));
388
+ const crcs = entries.map((e) => crc32(e.data));
389
+
390
+ let localSize = 0;
391
+ let centralSize = 0;
392
+ for (let i = 0; i < entries.length; i++) {
393
+ localSize += 30 + names[i]!.length + entries[i]!.data.length;
394
+ centralSize += 46 + names[i]!.length;
395
+ }
396
+
397
+ const out = new Uint8Array(localSize + centralSize + 22);
398
+ const view = new DataView(out.buffer);
399
+ const offsets: number[] = [];
400
+ let p = 0;
401
+
402
+ for (let i = 0; i < entries.length; i++) {
403
+ const { data } = entries[i]!;
404
+ const name = names[i]!;
405
+ offsets.push(p);
406
+ view.setUint32(p, LFH_SIG, true);
407
+ view.setUint16(p + 4, 10, true); // version needed: 1.0 (stored)
408
+ view.setUint16(p + 6, 0, true); // flags
409
+ view.setUint16(p + 8, 0, true); // method: stored
410
+ view.setUint16(p + 10, 0, true); // dos time
411
+ view.setUint16(p + 12, DOS_EPOCH_DATE, true);
412
+ view.setUint32(p + 14, crcs[i]!, true);
413
+ view.setUint32(p + 18, data.length, true); // compressed size
414
+ view.setUint32(p + 22, data.length, true); // uncompressed size
415
+ view.setUint16(p + 26, name.length, true);
416
+ view.setUint16(p + 28, 0, true); // extra length
417
+ out.set(name, p + 30);
418
+ out.set(data, p + 30 + name.length);
419
+ p += 30 + name.length + data.length;
420
+ }
421
+
422
+ const centralStart = p;
423
+ for (let i = 0; i < entries.length; i++) {
424
+ const { data } = entries[i]!;
425
+ const name = names[i]!;
426
+ view.setUint32(p, CDH_SIG, true);
427
+ view.setUint16(p + 4, 10, true); // version made by
428
+ view.setUint16(p + 6, 10, true); // version needed
429
+ view.setUint16(p + 8, 0, true); // flags
430
+ view.setUint16(p + 10, 0, true); // method: stored
431
+ view.setUint16(p + 12, 0, true); // dos time
432
+ view.setUint16(p + 14, DOS_EPOCH_DATE, true);
433
+ view.setUint32(p + 16, crcs[i]!, true);
434
+ view.setUint32(p + 20, data.length, true);
435
+ view.setUint32(p + 24, data.length, true);
436
+ view.setUint16(p + 28, name.length, true);
437
+ view.setUint16(p + 30, 0, true); // extra length
438
+ view.setUint16(p + 32, 0, true); // comment length
439
+ view.setUint16(p + 34, 0, true); // disk number
440
+ view.setUint16(p + 36, 0, true); // internal attrs
441
+ view.setUint32(p + 38, 0, true); // external attrs
442
+ view.setUint32(p + 42, offsets[i]!, true);
443
+ out.set(name, p + 46);
444
+ p += 46 + name.length;
445
+ }
446
+
447
+ view.setUint32(p, EOCD_SIG, true);
448
+ view.setUint16(p + 4, 0, true); // disk number
449
+ view.setUint16(p + 6, 0, true); // central directory start disk
450
+ view.setUint16(p + 8, entries.length, true);
451
+ view.setUint16(p + 10, entries.length, true);
452
+ view.setUint32(p + 12, centralSize, true);
453
+ view.setUint32(p + 16, centralStart, true);
454
+ view.setUint16(p + 20, 0, true); // comment length
455
+
456
+ return out;
457
+ }
458
+
459
+ function latin1(text: string): Uint8Array {
460
+ const out = new Uint8Array(text.length);
461
+ for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i) & 0xff;
462
+ return out;
463
+ }
package/src/romset.ts ADDED
Binary file