silphscope 1.4.2 → 1.4.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/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.4",
4
4
  "description": "A firered/leafgreen ROM asset extractor for use in web applications",
5
5
  "main": "main.js",
6
6
  "exports": {
@@ -26,9 +26,26 @@
26
26
  "!src/rom-map-parser.js",
27
27
  "!rom-maps/",
28
28
  "!graphics-maps/",
29
- "ball-data/"
29
+ "ball-data/",
30
+ "!src/object-event-data-parser.js"
31
+ ],
32
+ "keywords": [
33
+ "gba",
34
+ "gameboy-advance",
35
+ "pokemon",
36
+ "firered",
37
+ "pokemon-firered",
38
+ "leafgreen",
39
+ "pokemon-leafgreen",
40
+ "rom",
41
+ "asset",
42
+ "asset-extractor",
43
+ "sprite",
44
+ "sprite-extractor",
45
+ "extractor",
46
+ "gen3",
47
+ "generation-3"
30
48
  ],
31
- "keywords": [],
32
49
  "author": "chickenPoo",
33
50
  "license": "MIT",
34
51
  "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,
@@ -25,5 +26,7 @@ export const firered = {
25
26
  ballParticlePalTable: 0x40BFA8,
26
27
  ballAnimPicTable: 0x26056C,
27
28
  ballAnimPalTable: 0x2605CC, // any idea what I am doing next?
29
+ objectEventPicTable: 0x39FDB0,
30
+ objectEventPalTable: 0x3A5158, // so these tables... technically aren't tables (except for the palette one kinda...) they are actually pointers to objects which then contain children who actually contain another object which finally contains a pointer to the actual graphics... this will be fun... (anyway will work on this eventually...)
28
31
  } // if this works out... well hehe... :D
29
32
  }
@@ -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;
@@ -4,6 +4,7 @@
4
4
  import fs from "fs";
5
5
  import path from "path";
6
6
  import { fileURLToPath } from "url";
7
+ import { mapLimit } from "../map-limit.js";
7
8
  import { renderMon } from "./mons/render-mons.js";
8
9
  import { renderIcon } from "./icons/render-icons.js";
9
10
  import { renderTrainer } from "./trainers/render-trainers.js";
@@ -43,6 +44,16 @@ export async function renderAllMons(rom, options = {}) {
43
44
  const config = getRomConfig(rom);
44
45
  const reader = new RomReader(rom, config);
45
46
 
47
+ await mapLimit(Object.keys(providedMons), 4, async (monName) => { // so in theory we should be running the function 4 times concurrently now...
48
+ await renderMon(monName, providedMons, reader, rom, {
49
+ variant: ["normal", "shiny"],
50
+ icon,
51
+ footprint,
52
+ outputDir,
53
+ });
54
+ console.log(`Done: ${monName}`);
55
+ });
56
+ /* just going to leave this here for now in case I decide to make it so that you can toggle between concurrent usage and I suppose synchronous would be the correct term for this down here...
46
57
  for (const monName of Object.keys(providedMons)) {
47
58
  await renderMon(monName, providedMons, reader, rom, { // hopefully this is faster since we are no longer calling the function 4 times lol
48
59
  side: ["front", "back"],
@@ -53,6 +64,7 @@ export async function renderAllMons(rom, options = {}) {
53
64
  });
54
65
  console.log(`Done: ${monName}`);
55
66
  }
67
+ */
56
68
  }
57
69
 
58
70
  export async function renderAllIcons(rom, options = {}) {
@@ -130,6 +142,8 @@ export async function renderAllBalls(rom, options= {}) {
130
142
  balls: providedBalls = balls,
131
143
  outputDir = "./out",
132
144
  ballParticles = true,
145
+ renderMasterBallImage = true,
146
+ renderMasterBallParticleImage = true,
133
147
  } = options;
134
148
 
135
149
  fs.mkdirSync(outputDir, { recursive: true });
@@ -141,6 +155,8 @@ export async function renderAllBalls(rom, options= {}) {
141
155
  await renderBall(ballName, providedBalls, reader, rom, {
142
156
  outputDir,
143
157
  ballParticles,
158
+ renderMasterBallImage,
159
+ renderMasterBallParticleImage,
144
160
  });
145
161
  console.log(`Done: ${ballName}`);
146
162
  }
@@ -184,6 +200,8 @@ export async function renderAllGraphics(rom, options = {}) { // eventually I wil
184
200
  await renderAllBalls(rom, {
185
201
  outputDir: outputBallDir,
186
202
  ballParticles: true,
203
+ renderMasterBallImage: true,
204
+ renderMasterBallParticleImage: true,
187
205
  });
188
206
  }
189
207
 
@@ -74,9 +74,9 @@ export async function renderMonFoot(monName, mons, reader, rom, options = {}) {
74
74
 
75
75
  if (outputDir) {
76
76
  const dir = `${outputDir}/${monName}`;
77
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
77
+ await fs.promises.mkdir(dir, { recursive: true });
78
78
  const fileName = `${dir}/footprint.png`;
79
- fs.writeFileSync(fileName, pngBuffer);
79
+ await fs.promises.writeFile(fileName, pngBuffer);
80
80
  }
81
81
 
82
82
  return pngBuffer;
@@ -56,9 +56,9 @@ export async function renderMonIcon(monName, mons, reader, rom, options = {}) {
56
56
 
57
57
  if (outputDir) {
58
58
  const dir = `${outputDir}/${monName}`;
59
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
60
- fs.writeFileSync(`${dir}/icon_frame1.png`, buffer1);
61
- fs.writeFileSync(`${dir}/icon_frame2.png`, buffer2);
59
+ await fs.promises.mkdir(dir, { recursive: true });
60
+ await fs.promises.writeFile(`${dir}/icon_frame1.png`, buffer1);
61
+ await fs.promises.writeFile(`${dir}/icon_frame2.png`, buffer2);
62
62
  }
63
63
 
64
64
  return {
@@ -61,6 +61,9 @@ export async function renderMon(monName, mons, reader, rom, options = {}) {
61
61
  const results = [];
62
62
  const width = 64;
63
63
  const height = 64;
64
+ if (outputDir) {
65
+ await fs.promises.mkdir(dir, { recursive: true }); // why was I using existsSync... eh well "fixed?" now I guess... also I moved this out of the loop as you can see so it only has to run... 440 times now... instead of 4x that number :p
66
+ }
64
67
  for (const side of sides) {
65
68
  for (const variant of variants) {
66
69
  const image = render4bppImage({
@@ -76,9 +79,8 @@ export async function renderMon(monName, mons, reader, rom, options = {}) {
76
79
 
77
80
  if (outputDir) {
78
81
  const dir = `${outputDir}/${monName}`;
79
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); // I suppose it is better to have this out of the loop for performance but erm I would say its fine for now :p
80
82
  const fileName = `${dir}/${side}${variant === "shiny" ? "_shiny" : ""}.png`;
81
- fs.writeFileSync(fileName, pngBuffer);
83
+ await fs.promises.writeFile(fileName, pngBuffer);
82
84
  }
83
85
 
84
86
  results.push(pngBuffer);
@@ -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
  }
@@ -0,0 +1,25 @@
1
+ // Copyright (c) 2026 chickenPoo
2
+ // Licensed under the MIT License. See LICENSE file in project root.
3
+
4
+ export async function mapLimit(items, limit, mapper) { // I hope this works... in theory it should and it seems quite simple... it's just that I am garbage at async thingies...
5
+ const results = new Array(items.length);
6
+ let index = 0;
7
+
8
+ async function worker() {
9
+ while (true) {
10
+ const current = index++;
11
+
12
+ if (index >= items.length) return;
13
+
14
+ results[current] = await mapper(items[current], current);
15
+ }
16
+ }
17
+ await Promise.all(
18
+ Array.from(
19
+ { length: Math.min(limit, items.length) },
20
+ worker
21
+ )
22
+ );
23
+
24
+ return results;
25
+ }