silphscope 1.4.2 → 1.4.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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  still a WIP however hopefully with some more work I can get this into a workable state...
2
2
 
3
+ (Amazingly Newer!) Update:
4
+
5
+ so now ball extraction works! still need to cut the images up but that should be simple
6
+
7
+ (also move graphics are basically done except for ICE_CHUNK it is a weird image... and I don't know how I am going to cut it up... but everything else is working! that makes it sound like ICE_CHUNK doesn't work... which it does it just doesn't get nicely cut up)
8
+
3
9
  (Even Newer!) Update:
4
10
 
5
11
  moves now work... kinda... still working on getting it all the way done but it mostly works!
@@ -26,11 +32,12 @@ await renderAllGraphics(rom, {
26
32
  outputIconDir: "./Assets/Icons", // same thing here :p
27
33
  outputTrainerDir: "./Assets/Trainers", // ...
28
34
  outputMoveDir: "./Assets/Moves",
29
- sortUnusedMoves: true // just sorts the unused moves into a sub-directory
35
+ sortUnusedMoves: true, // just sorts the unused moves into a sub-directory
36
+ outputBallDir: "./Assets/Balls"
30
37
  });
31
38
  ```
32
39
 
33
- Of course though the above is for extracting all graphics (which is kinda a lie... In reality it only extracts mon images, item icons, trainer images, and move images... but like I said this is a WIP :p so wait a bit please!).
40
+ Of course though the above is for extracting all graphics (which is kinda a lie... In reality it only extracts mon images, item icons, trainer images, move images, and ball images... but like I said this is a WIP :p so wait a bit please!).
34
41
 
35
42
  But if you want say just the mon images or item icons refer below:
36
43
 
@@ -81,4 +88,15 @@ await renderAllMoves(rom, {
81
88
  renderMasterImage: true, // kinda forgot about this... basically it renders a uncut image of the move anim if you like
82
89
  sortUnused: true, // sorts unused moves into a sub-directory
83
90
  })
84
- ```
91
+ ```
92
+
93
+ ball image extraction:
94
+ ```JavaScript
95
+ import fs from "fs";
96
+ import { renderAllBalls } from "silphscope" // o-O
97
+
98
+ const rom = fs.readFileSync("./path/to/your/rom.gba") // the file path explains :/
99
+ await renderAllBalls(rom, {
100
+ outputDir: "./Assets/Balls",
101
+ ballParticles: true, // set to false if you don't want the ball particles :p
102
+ })
@@ -1,38 +1,156 @@
1
1
  {
2
2
  "POKE": {
3
- "index": 0
3
+ "index": 0,
4
+ "frameCount": 2,
5
+ "frames": [
6
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
7
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
8
+ ],
9
+ "particleFrameCount": 3,
10
+ "particleFrames": [
11
+ { "x": 0, "y": 0, "width": 8, "height": 8 },
12
+ { "x": 0, "y": 8, "width": 8, "height": 8 },
13
+ { "x": 0, "y": 16, "width": 8, "height": 8 }
14
+ ]
4
15
  },
5
16
  "GREAT": {
6
- "index": 1
17
+ "index": 1,
18
+ "frameCount": 2,
19
+ "frames": [
20
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
21
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
22
+ ],
23
+ "particleFrameCount": 2,
24
+ "particleFrames": [
25
+ { "x": 0, "y": 8, "width": 8, "height": 8 },
26
+ { "x": 0, "y": 16, "width": 8, "height": 8 }
27
+ ]
7
28
  },
8
29
  "SAFARI": {
9
- "index": 2
30
+ "index": 2,
31
+ "frameCount": 2,
32
+ "frames": [
33
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
34
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
35
+ ],
36
+ "particleFrameCount": 2,
37
+ "particleFrames": [
38
+ { "x": 0, "y": 0, "width": 8, "height": 8 },
39
+ { "x": 0, "y": 8, "width": 8, "height": 8 }
40
+ ]
10
41
  },
11
42
  "ULTRA": {
12
- "index": 3
43
+ "index": 3,
44
+ "frameCount": 2,
45
+ "frames": [
46
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
47
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
48
+ ],
49
+ "particleFrameCount": 1,
50
+ "particleFrames": [
51
+ { "x": 0, "y": 56, "width": 8, "height": 8 }
52
+ ]
13
53
  },
14
54
  "MASTER": {
15
- "index": 4
55
+ "index": 4,
56
+ "frameCount": 2,
57
+ "frames": [
58
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
59
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
60
+ ],
61
+ "particleFrameCount": 1,
62
+ "particleFrames": [
63
+ { "x": 0, "y": 24, "width": 8, "height": 8 }
64
+ ]
16
65
  },
17
66
  "NET": {
18
- "index": 5
67
+ "index": 5,
68
+ "frameCount": 2,
69
+ "frames": [
70
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
71
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
72
+ ],
73
+ "particleFrameCount": 1,
74
+ "particleFrames": [
75
+ { "x": 0, "y": 32, "width": 8, "height": 8 }
76
+ ]
19
77
  },
20
78
  "DIVE": {
21
- "index": 6
79
+ "index": 6,
80
+ "frameCount": 3,
81
+ "frames": [
82
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
83
+ { "x": 0, "y": 16, "width": 16, "height": 16 },
84
+ { "x": 0, "y": 32, "width": 16, "height": 16 }
85
+ ],
86
+ "particleFrameCount": 1,
87
+ "particleFrames": [
88
+ { "x": 0, "y": 32, "width": 8, "height": 8 }
89
+ ]
22
90
  },
23
91
  "NEST": {
24
- "index": 7
92
+ "index": 7,
93
+ "frameCount": 2,
94
+ "frames": [
95
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
96
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
97
+ ],
98
+ "particleFrameCount": 1,
99
+ "particleFrames": [
100
+ { "x": 0, "y": 40, "width": 8, "height": 8 }
101
+ ]
25
102
  },
26
103
  "REPEAT": {
27
- "index": 8
104
+ "index": 8,
105
+ "frameCount": 2,
106
+ "frames": [
107
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
108
+ { "x": 0, "y": 16, "width": 16, "height": 16 }
109
+ ],
110
+ "particleFrameCount": 1,
111
+ "particleFrames": [
112
+ { "x": 0, "y": 56, "width": 8, "height": 8 }
113
+ ]
28
114
  },
29
115
  "TIMER": {
30
- "index": 9
116
+ "index": 9,
117
+ "frameCount": 3,
118
+ "frames": [
119
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
120
+ { "x": 0, "y": 16, "width": 16, "height": 16 },
121
+ { "x": 0, "y": 32, "width": 16, "height": 16 }
122
+ ],
123
+ "particleFrameCount": 1,
124
+ "particleFrames": [
125
+ { "x": 0, "y": 56, "width": 8, "height": 8 }
126
+ ]
31
127
  },
32
128
  "LUXURY": {
33
- "index": 10
129
+ "index": 10,
130
+ "frameCount": 3,
131
+ "frames": [
132
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
133
+ { "x": 0, "y": 16, "width": 16, "height": 16 },
134
+ { "x": 0, "y": 32, "width": 16, "height": 16 }
135
+ ],
136
+ "particleFrameCount": 2,
137
+ "particleFrames": [
138
+ { "x": 0, "y": 48, "width": 8, "height": 8 },
139
+ { "x": 0, "y": 56, "width": 8, "height": 8 }
140
+ ]
34
141
  },
35
142
  "PREMIER": {
36
- "index": 11
143
+ "index": 11,
144
+ "frameCount": 3,
145
+ "frames": [
146
+ { "x": 0, "y": 0, "width": 16, "height": 16 },
147
+ { "x": 0, "y": 16, "width": 16, "height": 16 },
148
+ { "x": 0, "y": 32, "width": 16, "height": 16 }
149
+ ],
150
+ "particleFrameCount": 2,
151
+ "particleFrames": [
152
+ { "x": 0, "y": 48, "width": 8, "height": 8 },
153
+ { "x": 0, "y": 56, "width": 8, "height": 8 }
154
+ ]
37
155
  }
38
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silphscope",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "A firered/leafgreen ROM asset extractor for use in web applications",
5
5
  "main": "main.js",
6
6
  "exports": {
@@ -28,7 +28,23 @@
28
28
  "!graphics-maps/",
29
29
  "ball-data/"
30
30
  ],
31
- "keywords": [],
31
+ "keywords": [
32
+ "gba",
33
+ "gameboy-advance",
34
+ "pokemon",
35
+ "firered",
36
+ "pokemon-firered",
37
+ "leafgreen",
38
+ "pokemon-leafgreen",
39
+ "rom",
40
+ "asset",
41
+ "asset-extractor",
42
+ "sprite",
43
+ "sprite-extractor",
44
+ "extractor",
45
+ "gen3",
46
+ "generation-3"
47
+ ],
32
48
  "author": "chickenPoo",
33
49
  "license": "MIT",
34
50
  "type": "module",
@@ -3,8 +3,9 @@
3
3
 
4
4
  // I have an idea but it needs math D:
5
5
 
6
- export const firered = {
6
+ export const fireredRev0 = {
7
7
  code: "BPRE", // I find this weird... like what does BP stand for? is it: "Battle Pokemon"? no idea :o
8
+ rev: 0, // I probably should have mentioned I have been making this tool with a rev0 ROM... but oh well it isn't like people are actually using this tool at least right now so no one has probably run into the "why isn't m firered ROM working!" issue :p
8
9
  tables: { // so the idea is essentially instead of a big JSON like we have now we instead just view the table graphics and build from that... it in theory could be slower because more lookups instead of direct references but it should be more maintainable...
9
10
  monFrontSprites: 0x2350AC,
10
11
  monBackSprites: 0x23654C,
@@ -0,0 +1,12 @@
1
+ // Copyright (c) 2026 chickenPoo
2
+ // Licensed under the MIT License. See LICENSE file in project root.
3
+
4
+ // time to see if my goofy ideas payed off lol
5
+
6
+ export const fireredRev1 = {
7
+ code: "BPRE",
8
+ rev: 1,
9
+ tables: {
10
+ // will add these later :p
11
+ }
12
+ }
@@ -0,0 +1,10 @@
1
+ // Copyright (c) 2026 chickenPoo
2
+ // Licensed under the MIT License. See LICENSE file in project root.
3
+
4
+ export const leafgreenRev0 = {
5
+ code: "BPGE",
6
+ rev: 0,
7
+ tables: {
8
+ // :p
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // Copyright (c) 2026 chickenPoo
2
+ // Licensed under the MIT License. See LICENSE file in project root.
3
+
4
+ export const leafgreenRev1 = {
5
+ code: "BPGE",
6
+ rev: 1,
7
+ tables: {
8
+ // later...
9
+ }
10
+ }
@@ -1,14 +1,27 @@
1
1
  // Copyright (c) 2026 chickenPoo
2
2
  // Licensed under the MIT License. See LICENSE file in project root.
3
3
 
4
- import { firered } from "../rom-configs/firered.js";
4
+ import { fireredRev0 } from "../rom-configs/fireredRev0.js";
5
+ import { fireredRev1 } from "../rom-configs/fireredRev1.js";
6
+ import { leafgreenRev0 } from "../rom-configs/leafgreenRev0.js";
7
+ import { leafgreenRev1 } from "../rom-configs/leafgreenRev1.js";
5
8
 
6
- function detectRomCode(rom) {
7
- return new TextDecoder().decode(rom.slice(0xAC, 0xB0));
9
+ function detectRomInfo(rom) {
10
+ return {
11
+ code: new TextDecoder().decode(rom.slice(0xAC, 0xB0)),
12
+ rev: rom[0xBC],
13
+ }
8
14
  }
9
15
 
10
16
  export function getRomConfig(rom) {
11
- const code = detectRomCode(rom);
12
- if (code === firered.code) return firered; // eventually will add more and so on :p (by more I mean leafgreen lol)
13
- throw new Error(`Unsupported ROM: ${code}`);
17
+ const romInfo = detectRomInfo(rom);
18
+ if (romInfo.code === fireredRev0.code || romInfo.code === fireredRev1.code) {
19
+ if (romInfo.rev === fireredRev0.rev) return fireredRev0;
20
+ if (romInfo.rev === fireredRev1.rev) return fireredRev1;
21
+ }
22
+ if (romInfo.code === leafgreenRev0.code || romInfo.code === leafgreenRev1.code) { // yes I know checking both of the codes is redundant since they are the same but let me enjoy my useless non-DRY code ;)
23
+ if (romInfo.rev === leafgreenRev0.rev) return leafgreenRev0;
24
+ if (romInfo.rev === leafgreenRev1.rev) return leafgreenRev1;
25
+ }
26
+ throw new Error(`Unsupported ROM: ${romInfo.code} rev ${romInfo.rev}`); // should be evrything I think...
14
27
  }
@@ -15,8 +15,29 @@ const streamToBuffer = (stream) => new Promise((resolve, reject) => {
15
15
  stream.on("error", reject);
16
16
  })
17
17
 
18
+ const extractFrameFromImage = (imageData, fullWidth, frameData) => {
19
+ const { x, y, width, height } = frameData;
20
+ const frameImage = new Uint8ClampedArray(width * height * 4);
21
+
22
+ for (let row = 0; row < height; row++) {
23
+ for (let col = 0; col < width; col++) {
24
+ const srcIndex = ((y + row) * fullWidth + (x + col)) * 4;
25
+ const dstIndex = (row * width + col) * 4;
26
+ frameImage[dstIndex] = imageData[srcIndex];
27
+ frameImage[dstIndex + 1] = imageData[srcIndex + 1];
28
+ frameImage[dstIndex + 2] = imageData[srcIndex + 2];
29
+ frameImage[dstIndex + 3] = imageData[srcIndex + 3];
30
+ }
31
+ }
32
+
33
+ return frameImage;
34
+ };
35
+
18
36
  export async function renderBallParticle(ballName, balls, reader, rom, options = {}) {
19
- const { outputDir = null } = options;
37
+ const {
38
+ outputDir = null,
39
+ renderMasterBallParticleImage = false,
40
+ } = options;
20
41
  if (!rom || !(rom instanceof Uint8Array || Buffer.isBuffer(rom))) {
21
42
  throw new TypeError("renderBallParticle(..., rom) requires a ROM Buffer/Uint8Array");
22
43
  }
@@ -51,8 +72,20 @@ export async function renderBallParticle(ballName, balls, reader, rom, options =
51
72
  if (outputDir) {
52
73
  const dir = `${outputDir}/${ballName}`;
53
74
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
54
- const fileName = `${dir}/particle.png`;
55
- fs.writeFileSync(fileName, pngBuffer);
75
+ if (renderMasterBallParticleImage) {
76
+ fs.writeFileSync(`${dir}/master-particle.png`, pngBuffer);
77
+ }
78
+ for (let i = 0; i < ball.particleFrames.length; i++) {
79
+ const frame = ball.particleFrames[i];
80
+ const frameImageData = extractFrameFromImage(image, width, frame);
81
+
82
+ const png = new PNG({ width: frame.width, height: frame.height });
83
+ png.data = frameImageData;
84
+ const pngFrameBuffer = await streamToBuffer(png.pack());
85
+
86
+ const fileName = `${dir}/particle-${i}.png`;
87
+ fs.writeFileSync(fileName, pngFrameBuffer);
88
+ }
56
89
  }
57
90
 
58
91
  return pngBuffer;
@@ -16,10 +16,30 @@ const streamToBuffer = (stream) => new Promise((resolve, reject) => {
16
16
  stream.on("error", reject);
17
17
  })
18
18
 
19
+ const extractFrameFromImage = (imageData, fullWidth, frameData) => {
20
+ const { x, y, width, height } = frameData;
21
+ const frameImage = new Uint8ClampedArray(width * height * 4);
22
+
23
+ for (let row = 0; row < height; row++) {
24
+ for (let col = 0; col < width; col++) {
25
+ const srcIndex = ((y + row) * fullWidth + (x + col)) * 4;
26
+ const dstIndex = (row * width + col) * 4;
27
+ frameImage[dstIndex] = imageData[srcIndex];
28
+ frameImage[dstIndex + 1] = imageData[srcIndex + 1];
29
+ frameImage[dstIndex + 2] = imageData[srcIndex + 2];
30
+ frameImage[dstIndex + 3] = imageData[srcIndex + 3];
31
+ }
32
+ }
33
+
34
+ return frameImage;
35
+ };
36
+
19
37
  export async function renderBall(ballName, balls, reader, rom, options = {}) {
20
38
  const {
21
39
  outputDir = null,
22
40
  ballParticles = false,
41
+ renderMasterBallImage = false,
42
+ renderMasterBallParticleImage = false,
23
43
  } = options;
24
44
  if (!rom || !(rom instanceof Uint8Array || Buffer.isBuffer(rom))) {
25
45
  throw new TypeError("renderBall(..., rom) requires a ROM Buffer/Uint8Array");
@@ -30,11 +50,14 @@ export async function renderBall(ballName, balls, reader, rom, options = {}) {
30
50
  throw new Error(`Missing Ball: ${ballName}`);
31
51
  }
32
52
  if (ballParticles) {
33
- renderBallParticle(ballName, balls, reader, rom, { outputDir });
53
+ renderBallParticle(ballName, balls, reader, rom, {
54
+ outputDir,
55
+ renderMasterBallParticleImage,
56
+ });
34
57
  }
35
58
  const ballPal = resolveBallSpritePal(ball, reader, ballName);
36
59
  const ballPic = resolveBallSpritePic(ball, reader, ballName);
37
- if(!ballPal || !ballPic) {
60
+ if (!ballPal || !ballPic) {
38
61
  throw new Error(`Missing assets for: ${ballName}`);
39
62
  }
40
63
 
@@ -57,8 +80,20 @@ export async function renderBall(ballName, balls, reader, rom, options = {}) {
57
80
  if (outputDir) {
58
81
  const dir = `${outputDir}/${ballName}`;
59
82
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
60
- const fileName = `${dir}/ball.png`;
61
- fs.writeFileSync(fileName, pngBuffer);
83
+ if (renderMasterBallImage) {
84
+ fs.writeFileSync(`${dir}/master-image.png`, pngBuffer)
85
+ }
86
+ for (let i = 0; i < ball.frames.length; i++) {
87
+ const frame = ball.frames[i];
88
+ const frameImageData = extractFrameFromImage(image, width, frame);
89
+
90
+ const png = new PNG({ width: frame.width, height: frame.height });
91
+ png.data = frameImageData;
92
+ const pngFrameBuffer = await streamToBuffer(png.pack());
93
+
94
+ const fileName = `${dir}/frame-${i}.png`;
95
+ fs.writeFileSync(fileName, pngFrameBuffer);
96
+ }
62
97
  }
63
98
 
64
99
  return pngBuffer;
@@ -130,6 +130,8 @@ export async function renderAllBalls(rom, options= {}) {
130
130
  balls: providedBalls = balls,
131
131
  outputDir = "./out",
132
132
  ballParticles = true,
133
+ renderMasterBallImage = true,
134
+ renderMasterBallParticleImage = true,
133
135
  } = options;
134
136
 
135
137
  fs.mkdirSync(outputDir, { recursive: true });
@@ -141,6 +143,8 @@ export async function renderAllBalls(rom, options= {}) {
141
143
  await renderBall(ballName, providedBalls, reader, rom, {
142
144
  outputDir,
143
145
  ballParticles,
146
+ renderMasterBallImage,
147
+ renderMasterBallParticleImage,
144
148
  });
145
149
  console.log(`Done: ${ballName}`);
146
150
  }
@@ -184,6 +188,8 @@ export async function renderAllGraphics(rom, options = {}) { // eventually I wil
184
188
  await renderAllBalls(rom, {
185
189
  outputDir: outputBallDir,
186
190
  ballParticles: true,
191
+ renderMasterBallImage: true,
192
+ renderMasterBallParticleImage: true,
187
193
  });
188
194
  }
189
195
 
@@ -68,6 +68,10 @@ export async function renderMove(moveName, moves, reader, rom, options = {}) {
68
68
  height,
69
69
  });
70
70
 
71
+ const png = new PNG({ width, height });
72
+ png.data = image;
73
+ const pngBuffer = await streamToBuffer(png.pack());
74
+
71
75
  if (outputDir) { // I will update this later but in theory it should also work... eventually though it will need a split inside to handle full image generation :p
72
76
  const rootDir = (sortUnused && move?.unused === true)? `${outputDir}/unused` : `${outputDir}`
73
77
  const dir = `${rootDir}/${moveName}`;
@@ -90,4 +94,6 @@ export async function renderMove(moveName, moves, reader, rom, options = {}) {
90
94
  fs.writeFileSync(fileName, pngBuffer);
91
95
  }
92
96
  }
97
+
98
+ return pngBuffer;
93
99
  }