roomie 1.0.2 → 1.0.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nicolás Contreras
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,15 +1,44 @@
1
1
  # roomie
2
2
 
3
- Roomie is a library for analyzing basic metadata of ROM files from various classic consoles. It allows extracting relevant information such as the game name, region, code, ROM and RAM size, version, and other console-specific data.
3
+ ![GitHub Sponsors](https://img.shields.io/github/sponsors/nikitacontreras?style=flat-square&label=sponsor%20me&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fnikitacontreras) ![NPM Version](https://img.shields.io/npm/v/roomie?style=flat-square)
4
+
5
+ ---
6
+
7
+ ## Introduction
8
+
9
+ **roomie** is a lightweight library for extracting metadata from ROM files of classic gaming consoles. It supports multiple systems and provides detailed information such as game title, region, game code, ROM and RAM sizes, version, and other console-specific data. Designed for simplicity and accuracy, roomie aids developers and enthusiasts in analyzing ROM files programmatically.
10
+
11
+ ---
4
12
 
5
13
  ## Installation
6
14
 
15
+ Install via npm:
16
+
7
17
  ```bash
8
18
  npm install roomie
9
19
  ```
10
20
 
21
+ ---
22
+
11
23
  ## Usage
12
24
 
25
+ roomie supports both CommonJS (CJS) and ES Modules (ESM) import styles.
26
+
27
+ ### CommonJS (CJS)
28
+
29
+ ```js
30
+ const Roomie = require("roomie");
31
+
32
+ const romPath = "/path/to/game.sfc";
33
+ const roomie = new Roomie(romPath);
34
+
35
+ roomie.on("loaded", (info) => {
36
+ console.log("ROM Information:", info);
37
+ });
38
+ ```
39
+
40
+ ### ES Modules (ESM)
41
+
13
42
  ```ts
14
43
  import Roomie from "roomie";
15
44
 
@@ -28,24 +57,72 @@ await roomie.load(romBuffer);
28
57
  console.log(roomie.info);
29
58
  ```
30
59
 
60
+ ---
61
+
31
62
  ## Supported Consoles
32
63
 
33
- Roomie supports metadata extraction from the following systems:
64
+ | Console | Description |
65
+ |-----------------------------|---------------------------------------------------------------|
66
+ | **Nintendo DS (NDS)** | Extracts game name, region, game code, ROM/RAM size, version, and other metadata. |
67
+ | **Game Boy Advance (GBA)** | Provides title, game code, region, ROM/RAM size, version, and related info. |
68
+ | **Game Boy (GB)** | Retrieves title, cartridge type, ROM/RAM size, and additional metadata. |
69
+ | **Super Nintendo / Super Famicom (SNES/SFC)** | Detects ROM type (HiROM/LoROM), game name, region, code, ROM size, and console-specific fields. |
70
+
71
+ ---
72
+
73
+ ## API Reference
74
+
75
+ ### Constructor
76
+
77
+ ```ts
78
+ new Roomie(pathOrBuffer: string | Buffer)
79
+ ```
80
+
81
+ Creates a new Roomie instance and immediately loads the ROM from the given file path or Buffer. Emits the `'loaded'` event once metadata extraction is complete.
82
+
83
+ ### Methods
84
+
85
+ ```ts
86
+ await roomie.load(pathOrBuffer: string | Buffer): Promise<void>
87
+ ```
88
+
89
+ Loads or reloads a ROM from a file path or Buffer, emitting `'loaded'` on success.
90
+
91
+ ### Properties
92
+
93
+ - `roomie.info: RomInfo` – Object containing extracted ROM metadata.
94
+ - `roomie.rom: Buffer` – Raw bytes of the loaded ROM.
95
+ - `roomie.system: "nds" | "gba" | "gb" | "sfc"` – Detected console system.
96
+
97
+ ### Events
98
+
99
+ - `'loaded'` – Emitted when ROM metadata is successfully loaded and parsed. The listener receives the `info` object.
100
+
101
+ ### Example JSON Output
102
+
103
+ ```json
104
+ {
105
+ "system": "gba",
106
+ "title": "METROID FUSION",
107
+ "gameCode": "AGB-AMME",
108
+ "region": "USA",
109
+ "romSize": 2097152,
110
+ "ramSize": 32768,
111
+ "version": 1,
112
+ "checksum": "0x1234"
113
+ }
114
+ ```
115
+
116
+ ---
34
117
 
35
- - **Nintendo DS (NDS):** Retrieves data such as the game name, region, game code, ROM and RAM size, version, among others.
36
- - **Game Boy Advance (GBA):** Extracts information about the title, game code, region, ROM and RAM size, version, etc.
37
- - **Game Boy (GB):** Provides details about the title, cartridge type, ROM and RAM size, and other metadata.
38
- - **Super Nintendo / Super Famicom (SNES/SFC):** Detects the ROM type (HiROM/LoROM), game name, region, code, ROM size, and other specific fields.
118
+ ## Error Handling
39
119
 
40
- ## API
120
+ If the ROM cannot be identified, the library throws errors with codes:
41
121
 
42
- - `new Roomie(path: string | Buffer)` – Creates an instance and immediately loads the specified ROM file or buffer. Emits the `'loaded'` event when the information is available.
43
- - `await roomie.load(pathOrBuffer: string | Buffer)` – Loads or reloads a different ROM file from a file path or a Buffer.
44
- - `roomie.info: RomInfo` – Object with the analyzed ROM metadata, including system, size, game code, region, and other specific fields.
45
- - `roomie.rom: Buffer` – Contains the raw bytes of the loaded ROM file.
46
- - `roomie.system: "nds" | "gba" | "gb" | "sfc"` – Detected system based on the file extension and content.
122
+ - `unknown_file` – When loading from a file path and the system is unrecognized.
123
+ - `unknown_bytes` – When loading from a Buffer and the system is unrecognized.
47
124
 
48
- If the system cannot be identified, the library throws errors with codes `unknown_file` (when loading from a path) or `unknown_bytes` (when loading from a Buffer).
125
+ ---
49
126
 
50
127
  ## License
51
128
 
package/dist/index.cjs CHANGED
@@ -1 +1,4 @@
1
- module.exports = require("./index.mjs");
1
+
2
+ const mod = require("./index.mjs");
3
+ module.exports = mod.default || mod;
4
+ module.exports.default = mod.default || mod;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { Roomie as default } from "./roomie";
2
- export * from "./roomie";
3
- export * from "./types";
1
+ import Roomie from "./roomie.js";
2
+ export default Roomie;
3
+ export * from "./roomie.js";
4
+ export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
- export { Roomie as default } from "./roomie";
2
- export * from "./roomie";
3
- export * from "./types";
1
+ import Roomie from "./roomie.js";
2
+ export default Roomie;
3
+ export * from "./roomie.js";
4
+ export * from "./types.js";
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
- export { Roomie as default } from "./roomie";
2
- export * from "./roomie";
3
- export * from "./types";
1
+ import Roomie from "./roomie.js";
2
+ export default Roomie;
3
+ export * from "./roomie.js";
4
+ export * from "./types.js";
package/dist/roomie.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { SupportedSystem } from "./types";
2
+ import type { SupportedSystem } from "./types.js";
3
3
  export interface RomInfo {
4
4
  path: string;
5
5
  system: SupportedSystem;
@@ -19,17 +19,32 @@ export interface RomInfo {
19
19
  ram?: number;
20
20
  hardware?: Record<string, unknown>;
21
21
  };
22
+ n64?: {
23
+ name?: string;
24
+ country?: string;
25
+ version?: string;
26
+ };
22
27
  }
23
28
  export declare class Roomie extends EventEmitter {
24
29
  private _path;
25
30
  private _rom;
26
31
  private _system;
27
32
  private _info;
33
+ name?: string;
34
+ gameid?: string;
35
+ region?: string;
36
+ gamecode?: string;
37
+ cartridge?: Record<string, unknown>;
28
38
  constructor(path: string);
29
39
  private detectSystemFromPath;
30
40
  private readGameCode;
31
41
  private computeRegion;
32
42
  private computeSfcInfo;
43
+ private _name;
44
+ private _gameid;
45
+ private _gamecode;
46
+ private _cartridge;
47
+ private _region;
33
48
  load(pathOrBuffer: string | Buffer): Promise<void>;
34
49
  get info(): RomInfo;
35
50
  get system(): SupportedSystem;
package/dist/roomie.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { createHash } from "node:crypto";
3
3
  import { promises as fs } from "node:fs";
4
- import { regions } from "./tables/regions";
5
- import { specs } from "./tables/specs";
6
- import { isHiRomBuffer } from "./systems/snes";
4
+ import { regions } from "./tables/regions.js";
5
+ import { specs } from "./tables/specs.js";
6
+ import { isHiRomBuffer } from "./systems/snes.js";
7
7
  export class Roomie extends EventEmitter {
8
8
  constructor(path) {
9
9
  super();
@@ -19,6 +19,8 @@ export class Roomie extends EventEmitter {
19
19
  return "gb";
20
20
  if (ext === "sfc" || ext === "smc")
21
21
  return "sfc";
22
+ if (ext === "z64" || ext === "n64")
23
+ return "n64";
22
24
  // Default to sfc to keep compatibility with original intent; could be improved.
23
25
  return "sfc";
24
26
  }
@@ -34,6 +36,9 @@ export class Roomie extends EventEmitter {
34
36
  if (system === "gb" && b.length >= 0x0143) {
35
37
  return b.subarray(0x013F, 0x0143).toString("ascii");
36
38
  }
39
+ if (system === "n64" && b.length >= 0x2F) {
40
+ return b.subarray(0x20, 0x2F).toString("ascii").trim();
41
+ }
37
42
  }
38
43
  catch { }
39
44
  return undefined;
@@ -66,40 +71,224 @@ export class Roomie extends EventEmitter {
66
71
  return regions.snes[key];
67
72
  }
68
73
  return undefined;
74
+ case "n64":
75
+ // N64 region code at offset 0x3E in some ROMs (common practice)
76
+ if (this._rom.length > 0x3E) {
77
+ const regionByte = this._rom[0x3E];
78
+ // Map region byte to region string (basic example)
79
+ const regionMap = {
80
+ 0x44: "USA",
81
+ 0x45: "Europe",
82
+ 0x46: "France",
83
+ 0x4A: "Japan",
84
+ 0x50: "PAL",
85
+ 0x55: "Australia",
86
+ 0x58: "Germany",
87
+ 0x59: "Europe",
88
+ 0x5A: "Europe",
89
+ };
90
+ return regionMap[regionByte] || "Unknown";
91
+ }
92
+ return undefined;
69
93
  }
70
94
  }
71
95
  computeSfcInfo() {
72
96
  if (this._system !== "sfc")
73
97
  return undefined;
74
98
  const hi = isHiRomBuffer(this._rom);
75
- const base = hi ? 0xFFD0 : 0x7FD0; // region/speed map nearby
76
- const romspeedOff = base + 0x09; // D9
77
- const romTypeOff = base + 0x05; // D5
78
- const romSizeOff = base + 0x07; // D7
79
- const ramSizeOff = base + 0x08; // D8
99
+ const base = hi ? 0xFFD0 : 0x7FD0;
100
+ const offD5 = base + 0x05; // map mode (for specs mapping)
101
+ const offD6 = base + 0x06; // hardware type
102
+ const offD7 = base + 0x07; // ROM size exponent
103
+ const offD8 = base + 0x08; // RAM size exponent
104
+ const offD9 = base + 0x09; // raw romSpeed byte
80
105
  const out = {};
81
- if (this._rom.length > romspeedOff) {
82
- const key = this._rom[romspeedOff].toString(16);
83
- out.romSpeed = key;
106
+ // romSpeed: raw D9 byte as string, like original JS
107
+ if (this._rom.length > offD9) {
108
+ out.romSpeed = this._rom[offD9].toString().trim();
109
+ }
110
+ // specs: map D5 (2-digit hex) to specs.sfc.romspeed
111
+ if (this._rom.length > offD5) {
112
+ const key = this._rom[offD5].toString(16).padStart(2, "0");
84
113
  const spec = specs.sfc?.romspeed?.[key];
85
114
  if (spec)
86
115
  out.rom = { ...(out.rom || {}), type: spec.type, speed: spec.speed };
87
116
  }
88
- if (this._rom.length > romTypeOff) {
89
- const hwKey = this._rom[romTypeOff].toString(16);
117
+ // rom size from D7 using original expression
118
+ if (this._rom.length > offD7) {
119
+ const exp = this._rom[offD7];
120
+ const size = 2 ** (2 ^ exp) * 1000;
121
+ out.rom = { ...(out.rom || {}), size };
122
+ }
123
+ // ram size from D8 using original expression
124
+ if (this._rom.length > offD8) {
125
+ const exp = this._rom[offD8];
126
+ out.ram = 2 ** (2 ^ exp) * 1000;
127
+ }
128
+ // hardware from D6 (2-digit hex)
129
+ if (this._rom.length > offD6) {
130
+ const hwKey = this._rom[offD6].toString(16).padStart(2, "0");
90
131
  const hw = specs.sfc?.hardware?.[hwKey];
91
132
  if (hw)
92
133
  out.hardware = hw;
93
134
  }
94
- if (this._rom.length > romSizeOff) {
95
- const exp = this._rom[romSizeOff];
96
- out.rom = { ...(out.rom || {}), size: Math.pow(2, exp) * 1024 };
135
+ return out;
136
+ }
137
+ _name() {
138
+ const b = this._rom;
139
+ try {
140
+ switch (this._system) {
141
+ case "nds":
142
+ if (b.length >= 0x20) {
143
+ return b.subarray(0x0, 0x20).toString("ascii").replace(/\0/g, "").trim();
144
+ }
145
+ break;
146
+ case "gba":
147
+ if (b.length >= 0xAC) {
148
+ return b.subarray(0xA0, 0xAC).toString("ascii").replace(/\0/g, "").trim();
149
+ }
150
+ break;
151
+ case "gb":
152
+ if (b.length >= 0x134) {
153
+ return b.subarray(0x134, 0x144).toString("ascii").replace(/\0/g, "").trim();
154
+ }
155
+ break;
156
+ case "sfc":
157
+ // SNES title at 0x7FC0 or 0xFFC0 depending on LoROM/HiROM
158
+ const hi = isHiRomBuffer(b);
159
+ const base = hi ? 0xFFC0 : 0x7FC0;
160
+ if (b.length > base + 21) {
161
+ return b.subarray(base, base + 21).toString("ascii").replace(/\0/g, "").trim();
162
+ }
163
+ break;
164
+ case "n64":
165
+ if (b.length >= 0x20) {
166
+ return b.subarray(0x20, 0x34).toString("ascii").replace(/\0/g, "").trim();
167
+ }
168
+ break;
169
+ }
97
170
  }
98
- if (this._rom.length > ramSizeOff) {
99
- const exp = this._rom[ramSizeOff];
100
- out.ram = Math.pow(2, exp) * 1024;
171
+ catch { }
172
+ return undefined;
173
+ }
174
+ _gameid() {
175
+ const code = this._gamecode();
176
+ if (!code)
177
+ return undefined;
178
+ switch (this._system) {
179
+ case "nds":
180
+ return "NTR-" + code;
181
+ case "gba":
182
+ return "AGB-" + code;
183
+ default:
184
+ return undefined;
101
185
  }
102
- return out;
186
+ }
187
+ _gamecode() {
188
+ const b = this._rom;
189
+ try {
190
+ switch (this._system) {
191
+ case "nds":
192
+ if (b.length >= 0x10) {
193
+ return b.subarray(0x0C, 0x10).toString("ascii");
194
+ }
195
+ break;
196
+ case "gba":
197
+ if (b.length >= 0xB0) {
198
+ return b.subarray(0xAC, 0xB0).toString("ascii");
199
+ }
200
+ break;
201
+ case "gb":
202
+ if (b.length >= 0x0143) {
203
+ return b.subarray(0x013F, 0x0143).toString("ascii");
204
+ }
205
+ break;
206
+ case "n64":
207
+ if (b.length >= 0x2F) {
208
+ return b.subarray(0x20, 0x2F).toString("ascii").trim();
209
+ }
210
+ break;
211
+ }
212
+ }
213
+ catch { }
214
+ return undefined;
215
+ }
216
+ _cartridge() {
217
+ // Build cartridge metadata depending on system
218
+ const b = this._rom;
219
+ switch (this._system) {
220
+ case "nds": {
221
+ const code = this._gamecode();
222
+ const region = this._region();
223
+ return {
224
+ system: "nds",
225
+ gameCode: code,
226
+ region,
227
+ size: b.length,
228
+ };
229
+ }
230
+ case "gba": {
231
+ const code = this._gamecode();
232
+ const region = this._region();
233
+ return {
234
+ system: "gba",
235
+ gameCode: code,
236
+ region,
237
+ size: b.length,
238
+ };
239
+ }
240
+ case "gb": {
241
+ const region = this._region();
242
+ return {
243
+ system: "gb",
244
+ region,
245
+ size: b.length,
246
+ };
247
+ }
248
+ case "sfc": {
249
+ const sfcInfo = this.computeSfcInfo();
250
+ return {
251
+ system: "sfc",
252
+ ...sfcInfo,
253
+ size: b.length,
254
+ };
255
+ }
256
+ case "n64": {
257
+ // N64 cartridge info from header bytes
258
+ const countryByte = b.length > 0x3E ? b[0x3E] : undefined;
259
+ const versionByte = b.length > 0x3F ? b[0x3F] : undefined;
260
+ const countryMap = {
261
+ 0x00: "Japan",
262
+ 0x01: "USA",
263
+ 0x02: "Europe",
264
+ 0x03: "Germany",
265
+ 0x04: "France",
266
+ 0x05: "Spain",
267
+ 0x06: "Italy",
268
+ 0x07: "China",
269
+ 0x08: "Australia",
270
+ 0x09: "Unknown",
271
+ 0x0A: "Unknown",
272
+ 0x0B: "Unknown",
273
+ 0x0C: "Unknown",
274
+ 0x0D: "Unknown",
275
+ 0x0E: "Unknown",
276
+ 0x0F: "Unknown",
277
+ };
278
+ return {
279
+ system: "n64",
280
+ name: this._name(),
281
+ country: countryByte !== undefined ? countryMap[countryByte] || "Unknown" : undefined,
282
+ version: versionByte !== undefined ? versionByte.toString() : undefined,
283
+ size: b.length,
284
+ };
285
+ }
286
+ default:
287
+ return undefined;
288
+ }
289
+ }
290
+ _region() {
291
+ return this.computeRegion(this._system, this._gamecode());
103
292
  }
104
293
  async load(pathOrBuffer) {
105
294
  if (typeof pathOrBuffer === "string") {
@@ -136,6 +325,13 @@ export class Roomie extends EventEmitter {
136
325
  detected = "gb";
137
326
  }
138
327
  }
328
+ // Check N64: ASCII text at 0x20-0x2E
329
+ if (!detected && b.length >= 0x2F) {
330
+ const code = b.subarray(0x20, 0x2F).toString("ascii");
331
+ if (/^[\x20-\x7E]+$/.test(code) && code.trim().length > 0) {
332
+ detected = "n64";
333
+ }
334
+ }
139
335
  // Check SFC: use isHiRomBuffer heuristic
140
336
  if (!detected) {
141
337
  if (b.length > 0x8000 && (isHiRomBuffer(b) || !isHiRomBuffer(b))) {
@@ -160,7 +356,19 @@ export class Roomie extends EventEmitter {
160
356
  if (this._system === "sfc") {
161
357
  info.sfc = this.computeSfcInfo();
162
358
  }
359
+ if (this._system === "n64") {
360
+ info.n64 = {
361
+ name: this._name(),
362
+ country: this._region(),
363
+ version: this._rom.length > 0x3F ? this._rom[0x3F].toString() : undefined,
364
+ };
365
+ }
163
366
  this._info = info;
367
+ this.name = this._name();
368
+ this.gameid = this._gameid();
369
+ this.region = this._region();
370
+ this.gamecode = this._gamecode();
371
+ this.cartridge = this._cartridge();
164
372
  this.emit("loaded", info);
165
373
  }
166
374
  get info() { return this._info; }
package/dist/types.d.ts CHANGED
@@ -1 +1 @@
1
- export type SupportedSystem = "nds" | "gba" | "gb" | "sfc";
1
+ export type SupportedSystem = "nds" | "gba" | "gb" | "sfc" | "n64";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roomie",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "ROM metadata helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  ".": {
12
12
  "require": "./dist/index.cjs",
13
13
  "import": "./dist/index.mjs",
14
- "types": "./dist/index.d.ts"
14
+ "default": "./dist/index.mjs"
15
15
  }
16
16
  },
17
17
  "engines": {