roomie 1.0.0 → 1.0.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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # roomie
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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install roomie
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import Roomie from "roomie";
15
+
16
+ const romPath = "/path/to/game.sfc";
17
+ const roomie = new Roomie(romPath);
18
+
19
+ roomie.on("loaded", (info) => {
20
+ console.log("ROM Information:", info);
21
+ });
22
+
23
+ await roomie.load(romPath); // Load ROM from file path
24
+
25
+ const romBuffer = Buffer.from([...]); // Load ROM from a Buffer
26
+ await roomie.load(romBuffer);
27
+
28
+ console.log(roomie.info);
29
+ ```
30
+
31
+ ## Supported Consoles
32
+
33
+ Roomie supports metadata extraction from the following systems:
34
+
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.
39
+
40
+ ## API
41
+
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.
47
+
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).
49
+
50
+ ## License
51
+
52
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./index.mjs");
@@ -0,0 +1,3 @@
1
+ export { Roomie as default } from "./roomie";
2
+ export * from "./roomie";
3
+ export * from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Roomie as default } from "./roomie";
2
+ export * from "./roomie";
3
+ export * from "./types";
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ export { Roomie as default } from "./roomie";
2
+ export * from "./roomie";
3
+ export * from "./types";
@@ -0,0 +1,39 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { SupportedSystem } from "./types";
3
+ export interface RomInfo {
4
+ path: string;
5
+ system: SupportedSystem;
6
+ size: number;
7
+ hash: {
8
+ sha1: string;
9
+ };
10
+ gameCode?: string;
11
+ region?: string;
12
+ sfc?: {
13
+ romSpeed?: string;
14
+ rom?: {
15
+ size?: number;
16
+ type?: string;
17
+ speed?: string;
18
+ };
19
+ ram?: number;
20
+ hardware?: Record<string, unknown>;
21
+ };
22
+ }
23
+ export declare class Roomie extends EventEmitter {
24
+ private _path;
25
+ private _rom;
26
+ private _system;
27
+ private _info;
28
+ constructor(path: string);
29
+ private detectSystemFromPath;
30
+ private readGameCode;
31
+ private computeRegion;
32
+ private computeSfcInfo;
33
+ load(pathOrBuffer: string | Buffer): Promise<void>;
34
+ get info(): RomInfo;
35
+ get system(): SupportedSystem;
36
+ get path(): string;
37
+ get rom(): Buffer;
38
+ }
39
+ export default Roomie;
package/dist/roomie.js ADDED
@@ -0,0 +1,171 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { createHash } from "node:crypto";
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";
7
+ export class Roomie extends EventEmitter {
8
+ constructor(path) {
9
+ super();
10
+ this.load(path);
11
+ }
12
+ detectSystemFromPath(p) {
13
+ const ext = p.toLowerCase().split(".").pop();
14
+ if (ext === "nds")
15
+ return "nds";
16
+ if (ext === "gba")
17
+ return "gba";
18
+ if (ext === "gb" || ext === "gbc")
19
+ return "gb";
20
+ if (ext === "sfc" || ext === "smc")
21
+ return "sfc";
22
+ // Default to sfc to keep compatibility with original intent; could be improved.
23
+ return "sfc";
24
+ }
25
+ readGameCode(system) {
26
+ const b = this._rom;
27
+ try {
28
+ if (system === "nds" && b.length >= 0x10) {
29
+ return b.subarray(0x0C, 0x10).toString("ascii");
30
+ }
31
+ if (system === "gba" && b.length >= 0xB0) {
32
+ return b.subarray(0xAC, 0xB0).toString("ascii");
33
+ }
34
+ if (system === "gb" && b.length >= 0x0143) {
35
+ return b.subarray(0x013F, 0x0143).toString("ascii");
36
+ }
37
+ }
38
+ catch { }
39
+ return undefined;
40
+ }
41
+ computeRegion(system, gameCode) {
42
+ switch (system) {
43
+ case "nds":
44
+ if (gameCode && gameCode.length >= 4) {
45
+ const key = gameCode[3];
46
+ return regions.nds[key];
47
+ }
48
+ return undefined;
49
+ case "gba":
50
+ if (gameCode && gameCode.length >= 4) {
51
+ const key = gameCode[3];
52
+ return regions.gba[key];
53
+ }
54
+ return undefined;
55
+ case "gb":
56
+ if (this._rom.length > 0x14A) {
57
+ const v = this._rom[0x14A];
58
+ return regions.gb[v];
59
+ }
60
+ return undefined;
61
+ case "sfc":
62
+ const hi = isHiRomBuffer(this._rom);
63
+ const off = hi ? 0xFFD9 : 0x7FD9;
64
+ if (this._rom.length > off) {
65
+ const key = this._rom[off];
66
+ return regions.snes[key];
67
+ }
68
+ return undefined;
69
+ }
70
+ }
71
+ computeSfcInfo() {
72
+ if (this._system !== "sfc")
73
+ return undefined;
74
+ 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
80
+ const out = {};
81
+ if (this._rom.length > romspeedOff) {
82
+ const key = this._rom[romspeedOff].toString(16);
83
+ out.romSpeed = key;
84
+ const spec = specs.sfc?.romspeed?.[key];
85
+ if (spec)
86
+ out.rom = { ...(out.rom || {}), type: spec.type, speed: spec.speed };
87
+ }
88
+ if (this._rom.length > romTypeOff) {
89
+ const hwKey = this._rom[romTypeOff].toString(16);
90
+ const hw = specs.sfc?.hardware?.[hwKey];
91
+ if (hw)
92
+ out.hardware = hw;
93
+ }
94
+ if (this._rom.length > romSizeOff) {
95
+ const exp = this._rom[romSizeOff];
96
+ out.rom = { ...(out.rom || {}), size: Math.pow(2, exp) * 1024 };
97
+ }
98
+ if (this._rom.length > ramSizeOff) {
99
+ const exp = this._rom[ramSizeOff];
100
+ out.ram = Math.pow(2, exp) * 1024;
101
+ }
102
+ return out;
103
+ }
104
+ async load(pathOrBuffer) {
105
+ if (typeof pathOrBuffer === "string") {
106
+ this._path = pathOrBuffer;
107
+ this._rom = await fs.readFile(pathOrBuffer);
108
+ this._system = this.detectSystemFromPath(pathOrBuffer);
109
+ if (!this._system) {
110
+ throw new Error("unknown_file");
111
+ }
112
+ }
113
+ else {
114
+ this._rom = pathOrBuffer;
115
+ this._path = "in-memory";
116
+ const b = this._rom;
117
+ let detected = undefined;
118
+ // Check NDS: game code at 0x0C-0x10 ASCII uppercase letters/digits
119
+ if (b.length >= 0x10) {
120
+ const code = b.subarray(0x0C, 0x10).toString("ascii");
121
+ if (/^[A-Z0-9]{4}$/.test(code)) {
122
+ detected = "nds";
123
+ }
124
+ }
125
+ // Check GBA: game code at 0xAC-0xB0 ASCII uppercase letters/digits
126
+ if (!detected && b.length >= 0xB0) {
127
+ const code = b.subarray(0xAC, 0xB0).toString("ascii");
128
+ if (/^[A-Z0-9]{4}$/.test(code)) {
129
+ detected = "gba";
130
+ }
131
+ }
132
+ // Check GB: game code at 0x0134-0x0143 ASCII valid characters
133
+ if (!detected && b.length >= 0x0143) {
134
+ const code = b.subarray(0x0134, 0x0143).toString("ascii");
135
+ if (/^[A-Z0-9]{4,9}$/.test(code)) {
136
+ detected = "gb";
137
+ }
138
+ }
139
+ // Check SFC: use isHiRomBuffer heuristic
140
+ if (!detected) {
141
+ if (b.length > 0x8000 && (isHiRomBuffer(b) || !isHiRomBuffer(b))) {
142
+ detected = "sfc";
143
+ }
144
+ }
145
+ if (!detected) {
146
+ throw new Error("unknown_bytes");
147
+ }
148
+ this._system = detected;
149
+ }
150
+ const sha1 = createHash("sha1").update(this._rom).digest("hex");
151
+ const gameCode = this.readGameCode(this._system);
152
+ const info = {
153
+ path: this._path,
154
+ system: this._system,
155
+ size: this._rom.length,
156
+ hash: { sha1 },
157
+ gameCode,
158
+ region: this.computeRegion(this._system, gameCode),
159
+ };
160
+ if (this._system === "sfc") {
161
+ info.sfc = this.computeSfcInfo();
162
+ }
163
+ this._info = info;
164
+ this.emit("loaded", info);
165
+ }
166
+ get info() { return this._info; }
167
+ get system() { return this._system; }
168
+ get path() { return this._path; }
169
+ get rom() { return this._rom; }
170
+ }
171
+ export default Roomie;
@@ -0,0 +1,3 @@
1
+ /** Heuristic detection. Defaults to LoROM (false) if unsure. */
2
+ export declare function isHiRom(path: string): Promise<boolean>;
3
+ export declare function isHiRomBuffer(buf: Buffer): boolean;
@@ -0,0 +1,31 @@
1
+ import { promises as fs } from "node:fs";
2
+ function plausibleHeaderByte(b) {
3
+ return Number.isInteger(b) && b >= 0 && b <= 0xFF;
4
+ }
5
+ /** Heuristic detection. Defaults to LoROM (false) if unsure. */
6
+ export async function isHiRom(path) {
7
+ const buf = await fs.readFile(path);
8
+ return isHiRomBuffer(buf);
9
+ }
10
+ export function isHiRomBuffer(buf) {
11
+ const offLo = 0x7FD5;
12
+ const offHi = 0xFFD5;
13
+ if (buf.length > offHi) {
14
+ const lo = buf[offLo];
15
+ const hi = buf[offHi];
16
+ if (plausibleHeaderByte(lo) && plausibleHeaderByte(hi)) {
17
+ // Very simple heuristic: choose the one whose "map" nibble looks like HiROM (0x21, 0x31, 0x23, 0x32, 0x25)
18
+ const hiromCandidates = new Set([0x21, 0x31, 0x23, 0x32, 0x25]);
19
+ const loromCandidates = new Set([0x20, 0x30]);
20
+ const hiLikely = hiromCandidates.has(hi);
21
+ const loLikely = loromCandidates.has(lo);
22
+ if (hiLikely && !loLikely)
23
+ return true;
24
+ if (loLikely && !hiLikely)
25
+ return false;
26
+ // If both plausible, prefer the one with a valid checksum complement just as a tie-breaker (not implemented)
27
+ }
28
+ }
29
+ // Fallback: LoROM
30
+ return false;
31
+ }
@@ -0,0 +1,46 @@
1
+ export declare const regions: {
2
+ readonly nds: {
3
+ readonly A: "asia";
4
+ readonly C: "china";
5
+ readonly P: "europe";
6
+ readonly E: "americas";
7
+ readonly J: "japan";
8
+ readonly F: "french";
9
+ readonly H: "dutch";
10
+ readonly I: "italian";
11
+ readonly K: "korean";
12
+ readonly L: "usa#2";
13
+ readonly M: "swedish";
14
+ readonly N: "norwegian";
15
+ readonly O: "international";
16
+ readonly Q: "danish";
17
+ readonly R: "russian";
18
+ readonly S: "spanish";
19
+ readonly T: "usa+aus";
20
+ readonly U: "australia";
21
+ readonly V: "eur+aus";
22
+ readonly W: "europe#3";
23
+ readonly X: "europe#4";
24
+ readonly Y: "europe#5";
25
+ readonly Z: "europe#5";
26
+ };
27
+ readonly gba: {
28
+ readonly J: "japan";
29
+ readonly E: "english";
30
+ readonly P: "europe";
31
+ readonly D: "german";
32
+ readonly F: "french";
33
+ readonly I: "italian";
34
+ readonly S: "spanish";
35
+ };
36
+ readonly gb: {
37
+ readonly 0: "japan";
38
+ readonly 1: "overseas";
39
+ };
40
+ readonly snes: {
41
+ readonly 0: "japan";
42
+ readonly 1: "americas";
43
+ readonly 2: "europe";
44
+ };
45
+ };
46
+ export type SupportedSystemsForRegions = keyof typeof regions;
@@ -1,47 +1,46 @@
1
- exports.regions = {
2
- nds: {
3
- "A": "asia",
4
- "C": "china",
5
- "P": "europe",
6
- "E": "americas",
7
- "J": "japan",
8
- "F": "french",
9
- "H": "dutch",
10
- "I": "italian",
11
- "J": "japanese",
12
- "K": "korean",
13
- "L": "usa#2",
14
- "M": "swedish",
15
- "N": "norwegian",
16
- "O": "international",
17
- "Q": "danish",
18
- "R": "russian",
19
- "S": "spanish",
20
- "T": "usa+aus",
21
- "U": "australia",
22
- "V": "eur+aus",
23
- "W": "europe#3",
24
- "X": "europe#4",
25
- "Y": "europe#5",
26
- "Z": "europe#5"
27
-
28
- },
29
- gba: {
30
- "J": "japan",
31
- "E": "english",
32
- "P": "europe",
33
- "D": "german",
34
- "F": "french",
35
- "I": "italian",
36
- "S": "spanish"
37
- },
38
- gb: {
39
- 0: "japan",
40
- 1: "overseas"
41
- },
42
- snes: {
43
- 0: "japan",
44
- 1: "americas",
45
- 2: "europe"
46
- }
47
- }
1
+ /* Auto-generated from original JS */
2
+ export const regions = {
3
+ nds: {
4
+ "A": "asia",
5
+ "C": "china",
6
+ "P": "europe",
7
+ "E": "americas",
8
+ "J": "japan",
9
+ "F": "french",
10
+ "H": "dutch",
11
+ "I": "italian",
12
+ "K": "korean",
13
+ "L": "usa#2",
14
+ "M": "swedish",
15
+ "N": "norwegian",
16
+ "O": "international",
17
+ "Q": "danish",
18
+ "R": "russian",
19
+ "S": "spanish",
20
+ "T": "usa+aus",
21
+ "U": "australia",
22
+ "V": "eur+aus",
23
+ "W": "europe#3",
24
+ "X": "europe#4",
25
+ "Y": "europe#5",
26
+ "Z": "europe#5"
27
+ },
28
+ gba: {
29
+ "J": "japan",
30
+ "E": "english",
31
+ "P": "europe",
32
+ "D": "german",
33
+ "F": "french",
34
+ "I": "italian",
35
+ "S": "spanish"
36
+ },
37
+ gb: {
38
+ 0: "japan",
39
+ 1: "overseas"
40
+ },
41
+ snes: {
42
+ 0: "japan",
43
+ 1: "americas",
44
+ 2: "europe"
45
+ }
46
+ };
@@ -0,0 +1,221 @@
1
+ export declare const specs: {
2
+ readonly nds: {
3
+ readonly unitcode: {
4
+ readonly "0": "nds";
5
+ readonly "1": "nds/dsi";
6
+ readonly "2": "dsi";
7
+ };
8
+ };
9
+ readonly gb: {
10
+ readonly a: "";
11
+ };
12
+ readonly sfc: {
13
+ readonly hardware: {
14
+ readonly "0": {
15
+ readonly coprocessor: false;
16
+ readonly rom: true;
17
+ };
18
+ readonly "1": {
19
+ readonly coprocessor: false;
20
+ readonly rom: true;
21
+ readonly ram: true;
22
+ };
23
+ readonly "2": {
24
+ readonly coprocessor: false;
25
+ readonly rom: true;
26
+ readonly ram: true;
27
+ readonly battery: true;
28
+ };
29
+ readonly "3": {
30
+ readonly coprocessor: "dsp";
31
+ readonly rom: true;
32
+ };
33
+ readonly "4": {
34
+ readonly coprocessor: "dsp";
35
+ readonly rom: true;
36
+ readonly ram: true;
37
+ };
38
+ readonly "5": {
39
+ readonly coprocessor: "dsp";
40
+ readonly rom: true;
41
+ readonly ram: true;
42
+ readonly battery: true;
43
+ };
44
+ readonly "6": {
45
+ readonly coprocessor: "dsp";
46
+ readonly rom: true;
47
+ readonly battery: true;
48
+ };
49
+ readonly "13": {
50
+ readonly coprocessor: "gsu/superFX";
51
+ readonly rom: true;
52
+ };
53
+ readonly "14": {
54
+ readonly coprocessor: "gsu/superFX";
55
+ readonly rom: true;
56
+ readonly ram: true;
57
+ };
58
+ readonly "15": {
59
+ readonly coprocessor: "gsu/superFX";
60
+ readonly rom: true;
61
+ readonly ram: true;
62
+ readonly battery: true;
63
+ };
64
+ readonly "16": {
65
+ readonly coprocessor: "gsu/superFX";
66
+ readonly rom: true;
67
+ readonly battery: true;
68
+ };
69
+ readonly "23": {
70
+ readonly coprocessor: "obc1";
71
+ readonly rom: true;
72
+ };
73
+ readonly "24": {
74
+ readonly coprocessor: "obc1";
75
+ readonly rom: true;
76
+ readonly ram: true;
77
+ };
78
+ readonly "25": {
79
+ readonly coprocessor: "obc1";
80
+ readonly rom: true;
81
+ readonly ram: true;
82
+ readonly battery: true;
83
+ };
84
+ readonly "26": {
85
+ readonly coprocessor: "obc1";
86
+ readonly rom: true;
87
+ readonly battery: true;
88
+ };
89
+ readonly "33": {
90
+ readonly coprocessor: "sa-1";
91
+ readonly rom: true;
92
+ };
93
+ readonly "34": {
94
+ readonly coprocessor: "sa-1";
95
+ readonly rom: true;
96
+ readonly ram: true;
97
+ };
98
+ readonly "35": {
99
+ readonly coprocessor: "sa-1";
100
+ readonly rom: true;
101
+ readonly ram: true;
102
+ readonly battery: true;
103
+ };
104
+ readonly "36": {
105
+ readonly coprocessor: "sa-1";
106
+ readonly rom: true;
107
+ readonly battery: true;
108
+ };
109
+ readonly "43": {
110
+ readonly coprocessor: "s-dd1";
111
+ readonly rom: true;
112
+ };
113
+ readonly "44": {
114
+ readonly coprocessor: "s-dd1";
115
+ readonly rom: true;
116
+ readonly ram: true;
117
+ };
118
+ readonly "45": {
119
+ readonly coprocessor: "s-dd1";
120
+ readonly rom: true;
121
+ readonly ram: true;
122
+ readonly battery: true;
123
+ };
124
+ readonly "46": {
125
+ readonly coprocessor: "s-dd1";
126
+ readonly rom: true;
127
+ readonly battery: true;
128
+ };
129
+ readonly "53": {
130
+ readonly coprocessor: "s-rtc";
131
+ readonly rom: true;
132
+ };
133
+ readonly "54": {
134
+ readonly coprocessor: "s-rtc";
135
+ readonly rom: true;
136
+ readonly ram: true;
137
+ };
138
+ readonly "55": {
139
+ readonly coprocessor: "s-rtc";
140
+ readonly rom: true;
141
+ readonly ram: true;
142
+ readonly battery: true;
143
+ };
144
+ readonly "56": {
145
+ readonly coprocessor: "s-rtc";
146
+ readonly rom: true;
147
+ readonly battery: true;
148
+ };
149
+ readonly e3: {
150
+ readonly coprocessor: "other";
151
+ readonly rom: true;
152
+ };
153
+ readonly e4: {
154
+ readonly coprocessor: "other";
155
+ readonly rom: true;
156
+ readonly ram: true;
157
+ };
158
+ readonly e5: {
159
+ readonly coprocessor: "other";
160
+ readonly rom: true;
161
+ readonly ram: true;
162
+ readonly battery: true;
163
+ };
164
+ readonly e6: {
165
+ readonly coprocessor: "other";
166
+ readonly rom: true;
167
+ readonly battery: true;
168
+ };
169
+ readonly f3: {
170
+ readonly coprocessor: "custom";
171
+ readonly rom: true;
172
+ };
173
+ readonly f4: {
174
+ readonly coprocessor: "custom";
175
+ readonly rom: true;
176
+ readonly ram: true;
177
+ };
178
+ readonly f5: {
179
+ readonly coprocessor: "custom";
180
+ readonly rom: true;
181
+ readonly ram: true;
182
+ readonly battery: true;
183
+ };
184
+ readonly f6: {
185
+ readonly coprocessor: "custom";
186
+ readonly rom: true;
187
+ readonly battery: true;
188
+ };
189
+ };
190
+ readonly romspeed: {
191
+ readonly "20": {
192
+ readonly type: "LoROM";
193
+ readonly speed: "2.68MHz";
194
+ };
195
+ readonly "21": {
196
+ readonly type: "HiROM";
197
+ readonly speed: "2.68MHz";
198
+ };
199
+ readonly "23": {
200
+ readonly type: "SA-1";
201
+ };
202
+ readonly "25": {
203
+ readonly type: "ExHiROM";
204
+ readonly speed: "2.68MHz";
205
+ };
206
+ readonly "30": {
207
+ readonly type: "LoROM";
208
+ readonly speed: "3.58MHz";
209
+ };
210
+ readonly "31": {
211
+ readonly type: "HiROM";
212
+ readonly speed: "3.58MHz";
213
+ };
214
+ readonly "32": {
215
+ readonly type: "ExHiROM";
216
+ readonly speed: "3.58MHz";
217
+ };
218
+ };
219
+ };
220
+ };
221
+ export type SupportedSystemsForSpecs = keyof typeof specs;
@@ -1,61 +1,61 @@
1
- exports.specs = {
2
- nds:{
3
- unitcode:{
4
- "0":"nds",
5
- "1": "nds/dsi",
6
- "2": "dsi"
7
- }
8
- },
9
- gb:{
10
- "a":""
11
- },
12
- sfc: {
13
- hardware: {
14
- "0": { coprocessor: false, rom: true },
15
- "1": { coprocessor: false, rom: true, ram: true },
16
- "2": { coprocessor: false, rom: true, ram: true, battery: true },
17
- "2": { coprocessor: false, rom: true, ram: true, battery: true },
18
- "3": { coprocessor: "dsp", rom: true },
19
- "4": { coprocessor: "dsp", rom: true, ram: true },
20
- "5": { coprocessor: "dsp", rom: true, ram: true, battery: true },
21
- "6": { coprocessor: "dsp", rom: true, battery: true },
22
- "13": { coprocessor: "gsu/superFX", rom: true },
23
- "14": { coprocessor: "gsu/superFX", rom: true, ram: true },
24
- "15": { coprocessor: "gsu/superFX", rom: true, ram: true, battery: true },
25
- "16": { coprocessor: "gsu/superFX", rom: true, battery: true },
26
- "23": { coprocessor: "obc1", rom: true },
27
- "24": { coprocessor: "obc1", rom: true, ram: true },
28
- "25": { coprocessor: "obc1", rom: true, ram: true, battery: true },
29
- "26": { coprocessor: "obc1", rom: true, battery: true },
30
- "33": { coprocessor: "sa-1", rom: true },
31
- "34": { coprocessor: "sa-1", rom: true, ram: true },
32
- "35": { coprocessor: "sa-1", rom: true, ram: true, battery: true },
33
- "36": { coprocessor: "sa-1", rom: true, battery: true },
34
- "43": { coprocessor: "s-dd1", rom: true },
35
- "44": { coprocessor: "s-dd1", rom: true, ram: true },
36
- "45": { coprocessor: "s-dd1", rom: true, ram: true, battery: true },
37
- "46": { coprocessor: "s-dd1", rom: true, battery: true },
38
- "53": { coprocessor: "s-rtc", rom: true },
39
- "54": { coprocessor: "s-rtc", rom: true, ram: true },
40
- "55": { coprocessor: "s-rtc", rom: true, ram: true, battery: true },
41
- "56": { coprocessor: "s-rtc", rom: true, battery: true },
42
- "e3": { coprocessor: "other", rom: true },
43
- "e4": { coprocessor: "other", rom: true, ram: true },
44
- "e5": { coprocessor: "other", rom: true, ram: true, battery: true },
45
- "e6": { coprocessor: "other", rom: true, battery: true },
46
- "f3": { coprocessor: "custom", rom: true },
47
- "f4": { coprocessor: "custom", rom: true, ram: true },
48
- "f5": { coprocessor: "custom", rom: true, ram: true, battery: true },
49
- "f6": { coprocessor: "custom", rom: true, battery: true },
50
- },
51
- romspeed: {
52
- "20": { type: "LoROM", speed: "2.68MHz" },
53
- "21": { type: "HiROM", speed: "2.68MHz" },
54
- "23": { type: "SA-1"},
55
- "25": { type: "ExHiROM", speed: "2.68MHz" },
56
- "30": { type: "LoROM", speed: "3.58MHz" },
57
- "31": { type: "HiROM", speed: "3.58MHz" },
58
- "32": { type: "ExHiROM", speed: "3.58MHz" },
59
- }
60
- }
61
- }
1
+ /* Auto-generated from original JS */
2
+ export const specs = {
3
+ nds: {
4
+ unitcode: {
5
+ "0": "nds",
6
+ "1": "nds/dsi",
7
+ "2": "dsi"
8
+ }
9
+ },
10
+ gb: {
11
+ "a": ""
12
+ },
13
+ sfc: {
14
+ hardware: {
15
+ "0": { coprocessor: false, rom: true },
16
+ "1": { coprocessor: false, rom: true, ram: true },
17
+ "2": { coprocessor: false, rom: true, ram: true, battery: true },
18
+ "3": { coprocessor: "dsp", rom: true },
19
+ "4": { coprocessor: "dsp", rom: true, ram: true },
20
+ "5": { coprocessor: "dsp", rom: true, ram: true, battery: true },
21
+ "6": { coprocessor: "dsp", rom: true, battery: true },
22
+ "13": { coprocessor: "gsu/superFX", rom: true },
23
+ "14": { coprocessor: "gsu/superFX", rom: true, ram: true },
24
+ "15": { coprocessor: "gsu/superFX", rom: true, ram: true, battery: true },
25
+ "16": { coprocessor: "gsu/superFX", rom: true, battery: true },
26
+ "23": { coprocessor: "obc1", rom: true },
27
+ "24": { coprocessor: "obc1", rom: true, ram: true },
28
+ "25": { coprocessor: "obc1", rom: true, ram: true, battery: true },
29
+ "26": { coprocessor: "obc1", rom: true, battery: true },
30
+ "33": { coprocessor: "sa-1", rom: true },
31
+ "34": { coprocessor: "sa-1", rom: true, ram: true },
32
+ "35": { coprocessor: "sa-1", rom: true, ram: true, battery: true },
33
+ "36": { coprocessor: "sa-1", rom: true, battery: true },
34
+ "43": { coprocessor: "s-dd1", rom: true },
35
+ "44": { coprocessor: "s-dd1", rom: true, ram: true },
36
+ "45": { coprocessor: "s-dd1", rom: true, ram: true, battery: true },
37
+ "46": { coprocessor: "s-dd1", rom: true, battery: true },
38
+ "53": { coprocessor: "s-rtc", rom: true },
39
+ "54": { coprocessor: "s-rtc", rom: true, ram: true },
40
+ "55": { coprocessor: "s-rtc", rom: true, ram: true, battery: true },
41
+ "56": { coprocessor: "s-rtc", rom: true, battery: true },
42
+ "e3": { coprocessor: "other", rom: true },
43
+ "e4": { coprocessor: "other", rom: true, ram: true },
44
+ "e5": { coprocessor: "other", rom: true, ram: true, battery: true },
45
+ "e6": { coprocessor: "other", rom: true, battery: true },
46
+ "f3": { coprocessor: "custom", rom: true },
47
+ "f4": { coprocessor: "custom", rom: true, ram: true },
48
+ "f5": { coprocessor: "custom", rom: true, ram: true, battery: true },
49
+ "f6": { coprocessor: "custom", rom: true, battery: true },
50
+ },
51
+ romspeed: {
52
+ "20": { type: "LoROM", speed: "2.68MHz" },
53
+ "21": { type: "HiROM", speed: "2.68MHz" },
54
+ "23": { type: "SA-1" },
55
+ "25": { type: "ExHiROM", speed: "2.68MHz" },
56
+ "30": { type: "LoROM", speed: "3.58MHz" },
57
+ "31": { type: "HiROM", speed: "3.58MHz" },
58
+ "32": { type: "ExHiROM", speed: "3.58MHz" },
59
+ }
60
+ }
61
+ };
@@ -0,0 +1 @@
1
+ export type SupportedSystem = "nds" | "gba" | "gb" | "sfc";
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function hexEncode(input: string): string;
@@ -0,0 +1,8 @@
1
+ export function hexEncode(input) {
2
+ let result = "";
3
+ for (let i = 0; i < input.length; i++) {
4
+ const hex = input.charCodeAt(i).toString(16);
5
+ result += ("000" + hex).slice(-4);
6
+ }
7
+ return result;
8
+ }
package/package.json CHANGED
@@ -1,36 +1,35 @@
1
1
  {
2
- "dependencies": {
3
- "cryptography": "^1.2.3",
4
- "path": "^0.12.7"
5
- },
6
2
  "name": "roomie",
7
- "version": "1.0.0",
8
- "description": "ROM analyzer for major consoles (NDS,GBA,GB,SNES...)",
9
- "main": "index.js",
10
- "directories": {
11
- "lib": "lib"
12
- },
13
- "devDependencies": {},
14
- "scripts": {
15
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "1.0.2",
4
+ "description": "ROM metadata helper",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.mjs",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "require": "./dist/index.cjs",
13
+ "import": "./dist/index.mjs",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
16
  },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/nikitacontreras/roomie.git"
17
+ "engines": {
18
+ "node": ">=18"
20
19
  },
21
- "keywords": [
22
- "rom",
23
- "emulation",
24
- "emurom",
25
- "snes",
26
- "gba",
27
- "nds",
28
- "nes"
20
+ "files": [
21
+ "dist"
29
22
  ],
30
- "author": "nikitacontreras",
31
- "license": "MIT",
32
- "bugs": {
33
- "url": "https://github.com/nikitacontreras/roomie/issues"
23
+ "scripts": {
24
+ "build": "tsc && node scripts/emit-dual.js",
25
+ "clean": "rimraf dist",
26
+ "prepublishOnly": "npm run build",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepare": "npm run clean && npm run build"
34
29
  },
35
- "homepage": "https://github.com/nikitacontreras/roomie#readme"
30
+ "devDependencies": {
31
+ "@types/node": "^20.10.0",
32
+ "rimraf": "^5.0.0",
33
+ "typescript": "^5.6.0"
34
+ }
36
35
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require("./lib/roomie")
package/lib/roomie.js DELETED
@@ -1,124 +0,0 @@
1
- const { createHash } = require("cryptography");
2
- const { EventEmitter } = require('events');
3
- const { regions } = require("./regions")
4
- const { specs } = require("./specs")
5
- const { stringHelper } = require("./stringHelper")
6
- const fs = require('fs');
7
- const { systems } = require('./system')
8
-
9
- let rom, system;
10
-
11
- class Roomie extends EventEmitter {
12
- constructor(path) {
13
- super()
14
- this.load(path)
15
-
16
- this.__rom, this.__system, this.__romInfo, this.__path
17
- this.name = this._name()
18
- this.gameid = this._gameid()
19
- this.region = this._region()
20
- this.gamecode = this._gamecode()
21
- this.cartridge = this._cartridge()
22
- }
23
- get system() {
24
- return this.__system
25
- }
26
- set system(extension) {
27
- this.__system = extension
28
- }
29
- get rom() {
30
- return this.__rom
31
- }
32
- set rom(buffer) {
33
- this.__rom = buffer
34
- }
35
-
36
- load = (path) => {
37
- this.__path = path
38
- this.__rom = fs.readFileSync(path)
39
- this.__romInfo = fs.statSync(path)
40
- this.__system = path.split(".")[path.split().length]
41
- }
42
-
43
- _name() {
44
- switch (this.system) {
45
- case "nds":
46
- return this.rom.slice(0x0, 0xB).toString().trim()
47
- case "gba":
48
- return this.rom.slice(0xA0, 0xA0 + 12).toString().trim()
49
- case "gb":
50
- return this.rom.slice(0x134, 0x134 + 9).toString().trim()
51
- case "sfc":
52
- return this.rom.slice(systems.snes.isHiRom(this.__path, 0x7FC0), systems.snes.isHiRom(this.__path, 0x7FC0) + 21).toString().trim()
53
- case "n64":
54
- return this.rom.slice(0x20, 0x20 + 14).toString().match(/.{1,2}/g).reverse().join('').split("").reverse().join("").trim()
55
- }
56
- }
57
-
58
- _gamecode() {
59
- switch (this.system) {
60
- case "nds":
61
- return this.rom.slice(0xD, 0xD + 2).toString()
62
- case "gba":
63
- return this.rom.slice(0xAD, 0xAD + 2).toString()
64
- case "sfc":
65
- return this.rom.slice(systems.snes.isHiRom(this.__path, 0x7FB2), systems.snes.isHiRom(this.__path, 0x7FB2) + 4).toString("ASCII").trim()
66
- }
67
- }
68
-
69
- _gameid() {
70
- switch (this.system) {
71
- case "nds":
72
- return "NTR-" + this.rom.slice(0xC, 0xC + 4).toString()
73
- case "gba":
74
- return "AGB-" + this.rom.slice(0xAC, 0xAC + 4).toString()
75
- }
76
- }
77
-
78
- _cartridge() {
79
- switch (this.system) {
80
- case "nds":
81
- return {
82
- unit: specs.nds.unitcode[this.rom[0x012].toString(16)],
83
- developer: this.rom[0x012].toString(16),
84
- version: this.rom[0x01E],
85
- title: this.rom.slice(0x265440, 0x265440 + 0x3B).toString().replace(/\x00/g, '').replace(/(\\n)/g, "\n")
86
- }
87
- case "gba":
88
- return {
89
- developer: this.rom.slice(0xB0, 0xB0 + 0x2).toString()
90
- }
91
- case "gb":
92
- return {
93
- rom: {
94
- size: this.rom[0x148]
95
- }
96
- }
97
- case "sfc":
98
- return {
99
- romSpeed: (systems.snes.isHiRom(this.__path) ? this.rom[0xFFD9] : this.rom[0x7FD9]).toString().trim(),
100
- rom: {
101
- size: 2 ** (2 ^ this.rom[systems.snes.isHiRom(this.__path, 0x7FD7)]) * 1000,
102
- specs: specs.sfc.romspeed[this.rom[systems.snes.isHiRom(this.__path, 0x7FD5)].toString(16)]
103
- },
104
- ram: 2 ** (2 ^ (this.rom[systems.snes.isHiRom(this.__path, 0x7FD8)])) * 1000,
105
- hardware: specs.sfc.hardware[this.rom[systems.snes.isHiRom(this.__path, 0x7FD6)].toString(16)],
106
- }
107
- }
108
- }
109
-
110
- _region() {
111
- switch (this.system) {
112
- case "nds":
113
- return regions.nds[(this.gameid[this.gameid.length - 1])]
114
- case "gba":
115
- return regions.gba[(this.gameid[this.gameid.length - 1])]
116
- case "gb":
117
- return regions.gb[this.rom[0x14A].toString()]
118
- case "sfc":
119
- return regions.snes[this.rom[systems.snes.isHiRom(this.__path, 0x7FD9)].toString()]
120
- }
121
- }
122
- }
123
-
124
- module.exports = Roomie
@@ -1,13 +0,0 @@
1
- module.exports = () => {
2
- String.prototype.hexEncode = function () {
3
- var hex, i;
4
-
5
- var result = "";
6
- for (i = 0; i < this.length; i++) {
7
- hex = this.charCodeAt(i).toString(16);
8
- result += ("000" + hex).slice(-4);
9
- }
10
-
11
- return result
12
- }
13
- }
package/lib/system.js DELETED
@@ -1,10 +0,0 @@
1
- const fs = require("fs")
2
- files = {}
3
- fs.readdirSync(`${__dirname}/systems`)
4
- .filter((module) => {
5
- return module.slice(module.length - 3) === '.js';
6
- })
7
- .forEach((module) => {
8
- files[module.split(".")[0]] = require(`${__dirname}/systems/` + module);
9
- });
10
- module.exports.systems = files
@@ -1,10 +0,0 @@
1
- const fs = require("fs")
2
-
3
- module.exports = {
4
- isHiRom: (path, hex) => {
5
- return fs.statSync(path).size % fs.readFileSync(path)[0x400] === 0 ? hex + 0x8000 : hex
6
- },
7
- name: (file) => {
8
- this.isHiRom()
9
- }
10
- }