roomie 1.0.2 → 1.0.3

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,5 +1,9 @@
1
1
  # roomie
2
2
 
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
+
3
7
  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.
4
8
 
5
9
  ## Installation
@@ -10,6 +14,23 @@ npm install roomie
10
14
 
11
15
  ## Usage
12
16
 
17
+ Roomie supports both CommonJS and ES Module import styles depending on your project setup.
18
+
19
+ ### CommonJS (CJS)
20
+
21
+ ```js
22
+ const { Roomie } = require('roomie');
23
+
24
+ const romPath = "/path/to/game.sfc";
25
+ const roomie = new Roomie(romPath);
26
+
27
+ roomie.on("loaded", (info) => {
28
+ console.log("ROM Information:", info);
29
+ });
30
+ ```
31
+
32
+ ### ES Modules (ESM)
33
+
13
34
  ```ts
14
35
  import Roomie from "roomie";
15
36
 
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.3",
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": {