roomie 1.1.1 → 1.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.
- package/dist/roomie.js +39 -45
- package/package.json +6 -7
package/dist/roomie.js
CHANGED
|
@@ -2,9 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
|
-
import
|
|
6
|
-
// Handle ESM/CJS Interop for AdmZip
|
|
7
|
-
const Zip = AdmZip.default || AdmZip;
|
|
5
|
+
import JSZip from "jszip";
|
|
8
6
|
import { regions } from "./tables/regions.js";
|
|
9
7
|
import { specs } from "./tables/specs.js";
|
|
10
8
|
import { isHiRomBuffer } from "./systems/snes.js";
|
|
@@ -453,42 +451,38 @@ export class Roomie extends EventEmitter {
|
|
|
453
451
|
return this.computeRegion(this._system, this._gamecode());
|
|
454
452
|
}
|
|
455
453
|
async load(pathOrBuffer) {
|
|
456
|
-
let b;
|
|
457
454
|
if (typeof pathOrBuffer === "string") {
|
|
458
455
|
this._path = pathOrBuffer;
|
|
459
|
-
|
|
456
|
+
let fileBuffer = await fs.readFile(pathOrBuffer);
|
|
460
457
|
// Check if it's a ZIP by extension or magic
|
|
461
458
|
if (pathOrBuffer.toLowerCase().endsWith(".zip") || (fileBuffer.length > 4 && fileBuffer.readUInt32BE(0) === 0x504B0304)) {
|
|
462
|
-
const zip =
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
const romEntry = entries.find((e) => !e.isDirectory && !e.entryName.match(/\.(txt|jpg|png|xml|db|url)$|^\./i));
|
|
466
|
-
if (!romEntry)
|
|
459
|
+
const zip = await JSZip.loadAsync(fileBuffer);
|
|
460
|
+
const romFilename = Object.keys(zip.files).find(f => !zip.files[f].dir && !f.match(/\.(txt|jpg|png|xml|db|url|json)$|^\./i));
|
|
461
|
+
if (!romFilename)
|
|
467
462
|
throw new Error("no_rom_in_zip");
|
|
468
|
-
|
|
463
|
+
fileBuffer = Buffer.from(await zip.files[romFilename].async("nodebuffer"));
|
|
464
|
+
pathOrBuffer = romFilename;
|
|
469
465
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
this._rom = b;
|
|
474
|
-
const detected = this.detectSystemFromPath(pathOrBuffer);
|
|
475
|
-
this._system = detected || "sfc"; // Fallback to be handled by buffer check if needed
|
|
466
|
+
this._rom = fileBuffer;
|
|
467
|
+
const detectedByPath = this.detectSystemFromPath(pathOrBuffer);
|
|
468
|
+
this._system = detectedByPath || "sfc";
|
|
476
469
|
}
|
|
477
|
-
else {
|
|
470
|
+
else if (Buffer.isBuffer(pathOrBuffer)) {
|
|
478
471
|
this._path = "in-memory";
|
|
479
|
-
// Check if buffer is a ZIP
|
|
472
|
+
// Check if buffer is a ZIP (0x504B0304)
|
|
480
473
|
if (pathOrBuffer.length > 4 && pathOrBuffer.readUInt32BE(0) === 0x504B0304) {
|
|
481
|
-
const zip =
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
if (!romEntry)
|
|
474
|
+
const zip = await JSZip.loadAsync(pathOrBuffer);
|
|
475
|
+
const romFilename = Object.keys(zip.files).find(f => !zip.files[f].dir && !f.match(/\.(txt|jpg|png|xml|db|url|json)$|^\./i));
|
|
476
|
+
if (!romFilename)
|
|
485
477
|
throw new Error("no_rom_in_zip");
|
|
486
|
-
|
|
478
|
+
this._rom = Buffer.from(await zip.files[romFilename].async("nodebuffer"));
|
|
487
479
|
}
|
|
488
480
|
else {
|
|
489
|
-
|
|
481
|
+
this._rom = pathOrBuffer;
|
|
490
482
|
}
|
|
491
|
-
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
throw new Error("Invalid path or buffer provided to Roomie.load()");
|
|
492
486
|
}
|
|
493
487
|
const romBuffer = this._rom;
|
|
494
488
|
let detected = undefined;
|
|
@@ -502,43 +496,43 @@ export class Roomie extends EventEmitter {
|
|
|
502
496
|
}
|
|
503
497
|
}
|
|
504
498
|
// Check GBA: game code at 0xAC-0xB0 ASCII uppercase letters/digits
|
|
505
|
-
if (!detected &&
|
|
506
|
-
const code =
|
|
499
|
+
if (!detected && romBuffer.length >= 0xB0) {
|
|
500
|
+
const code = romBuffer.subarray(0xAC, 0xB0).toString("ascii");
|
|
507
501
|
if (/^[A-Z0-9]{4}$/.test(code)) {
|
|
508
502
|
detected = "gba";
|
|
509
503
|
}
|
|
510
504
|
}
|
|
511
505
|
// Check GB: game code at 0x0134-0x0143 ASCII valid characters
|
|
512
|
-
if (!detected &&
|
|
513
|
-
const code =
|
|
506
|
+
if (!detected && romBuffer.length >= 0x0143) {
|
|
507
|
+
const code = romBuffer.subarray(0x0134, 0x0143).toString("ascii");
|
|
514
508
|
if (/^[A-Z0-9]{4,9}$/.test(code)) {
|
|
515
509
|
detected = "gb";
|
|
516
510
|
}
|
|
517
511
|
}
|
|
518
512
|
// Check NES: starts with "NES\x1a"
|
|
519
|
-
if (!detected &&
|
|
520
|
-
if (
|
|
513
|
+
if (!detected && romBuffer.length >= 4) {
|
|
514
|
+
if (romBuffer[0] === 0x4E && romBuffer[1] === 0x45 && romBuffer[2] === 0x53 && romBuffer[3] === 0x1A) {
|
|
521
515
|
detected = "nes";
|
|
522
516
|
}
|
|
523
517
|
}
|
|
524
518
|
// Check N64: ASCII text at 0x20-0x2E AND Magic Word at 0x00
|
|
525
|
-
if (!detected &&
|
|
526
|
-
const magic =
|
|
519
|
+
if (!detected && romBuffer.length >= 0x2F) {
|
|
520
|
+
const magic = romBuffer.readUInt32BE(0);
|
|
527
521
|
// Common N64 magic values (Big Endian, Byte Swapped, Little Endian)
|
|
528
522
|
if (magic === 0x80371240 || magic === 0x37804012 || magic === 0x40123780) {
|
|
529
523
|
detected = "n64";
|
|
530
524
|
}
|
|
531
525
|
}
|
|
532
526
|
// Check SFC: verify checksum or markup before falling back
|
|
533
|
-
if (!detected &&
|
|
534
|
-
if (isHiRomBuffer(
|
|
527
|
+
if (!detected && romBuffer.length >= 0x8000) {
|
|
528
|
+
if (isHiRomBuffer(romBuffer)) {
|
|
535
529
|
detected = "sfc";
|
|
536
530
|
}
|
|
537
531
|
else {
|
|
538
532
|
// LoROM check
|
|
539
533
|
const titleOff = 0x7FC0;
|
|
540
|
-
if (
|
|
541
|
-
const title =
|
|
534
|
+
if (romBuffer.length > titleOff + 20) {
|
|
535
|
+
const title = romBuffer.subarray(titleOff, titleOff + 20).toString('ascii');
|
|
542
536
|
if (/^[\x20-\x7E\s]+$/.test(title) && title.trim().length > 0) {
|
|
543
537
|
detected = "sfc";
|
|
544
538
|
}
|
|
@@ -546,8 +540,8 @@ export class Roomie extends EventEmitter {
|
|
|
546
540
|
}
|
|
547
541
|
}
|
|
548
542
|
// Check Genesis: "SEGA" at 0x100
|
|
549
|
-
if (!detected &&
|
|
550
|
-
const magic =
|
|
543
|
+
if (!detected && romBuffer.length >= 0x104) {
|
|
544
|
+
const magic = romBuffer.subarray(0x100, 0x104).toString("ascii");
|
|
551
545
|
if (magic === "SEGA") {
|
|
552
546
|
detected = "genesis";
|
|
553
547
|
}
|
|
@@ -555,16 +549,16 @@ export class Roomie extends EventEmitter {
|
|
|
555
549
|
// Check SMS/GG: "TMR SEGA" at 0x7FF0, 0x3FF0 or 0x1FF0
|
|
556
550
|
if (!detected) {
|
|
557
551
|
for (const off of [0x7FF0, 0x3FF0, 0x1FF0]) {
|
|
558
|
-
if (
|
|
559
|
-
detected =
|
|
552
|
+
if (romBuffer.length >= off + 8 && romBuffer.subarray(off, off + 8).toString("ascii") === "TMR SEGA") {
|
|
553
|
+
detected = romBuffer.length >= 0x8000 ? "sms" : "gg"; // Heuristic
|
|
560
554
|
break;
|
|
561
555
|
}
|
|
562
556
|
}
|
|
563
557
|
}
|
|
564
558
|
// Check WSC: check model byte near end
|
|
565
|
-
if (!detected &&
|
|
566
|
-
const off =
|
|
567
|
-
if (
|
|
559
|
+
if (!detected && romBuffer.length >= 0x8000) {
|
|
560
|
+
const off = romBuffer.length - 10;
|
|
561
|
+
if (romBuffer[off + 1] === 0 || romBuffer[off + 1] === 1) { // 0=WS, 1=WSC
|
|
568
562
|
// WSC check is a bit weak without developer ID check, but let's use it as heuristic
|
|
569
563
|
// maybe only if extension also matches?
|
|
570
564
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roomie",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "ROM metadata helper",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,9 +14,6 @@
|
|
|
14
14
|
"default": "./dist/index.mjs"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"adm-zip": "^0.5.10"
|
|
19
|
-
},
|
|
20
17
|
"engines": {
|
|
21
18
|
"node": ">=18"
|
|
22
19
|
},
|
|
@@ -31,9 +28,11 @@
|
|
|
31
28
|
"prepare": "npm run clean && npm run build"
|
|
32
29
|
},
|
|
33
30
|
"devDependencies": {
|
|
34
|
-
"@types/
|
|
35
|
-
"@types/node": "^20.10.0",
|
|
31
|
+
"@types/node": "^20.19.37",
|
|
36
32
|
"rimraf": "^5.0.0",
|
|
37
33
|
"typescript": "^5.6.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"jszip": "^3.10.1"
|
|
38
37
|
}
|
|
39
|
-
}
|
|
38
|
+
}
|