silphscope 1.2.30 → 1.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silphscope",
3
- "version": "1.2.30",
3
+ "version": "1.3.0",
4
4
  "description": "A firered/leafgreen ROM asset extractor for use in web applications",
5
5
  "main": "main.js",
6
6
  "exports": {
@@ -17,7 +17,8 @@
17
17
  "rom-maps/",
18
18
  "item-data/",
19
19
  "trainer-data",
20
- "rom-configs/"
20
+ "rom-configs/",
21
+ "move-data/"
21
22
  ],
22
23
  "keywords": [],
23
24
  "author": "chickenPoo",
@@ -19,5 +19,7 @@ export const firered = {
19
19
  trainerBackPicTable: 0x239FA4,
20
20
  trainerBackPicPaletteTable: 0x239FD4,
21
21
  itemIconTable: 0x3D4294, // newer update: (so turns out this old value was the pointer to the table lol however this new one is the actual table... for real this time!) rest of the old message: so this was very hard to find since it isn't labeled in the .map of ROMs... luckily Ghidra and the ROM decomps exist so that helped a ton... issue is I believe this contains both the palette and gfx in each listing (should probably double check pokefirered to confirm...) so it will be interesting to extract assets I suppose...
22
+ moveAnimPicTable: 0x3ACC08,
23
+ moveAnimPaletteTable: 0x3AD510,
22
24
  } // if this works out... well hehe... :D
23
25
  }
@@ -9,6 +9,7 @@ import { renderIcon } from "./icons/render-icons.js";
9
9
  import { renderTrainer } from "./trainers/render-trainers.js";
10
10
  import { RomReader } from "../rom-reader.js";
11
11
  import { getRomConfig } from "../get-rom-config.js";
12
+ import { renderMove } from "./moves/render-moves.js";
12
13
 
13
14
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
14
15
 
@@ -17,11 +18,11 @@ function loadDefaultJson(relativePath) {
17
18
  return JSON.parse(fs.readFileSync(absolutePath, "utf-8"));
18
19
  }
19
20
 
20
- const assets = loadDefaultJson("../graphics-maps/fr-graphic-map.json");
21
21
  const mons = loadDefaultJson("../mon-data/monData.json");
22
22
  const icons = loadDefaultJson("../item-data/itemData.json");
23
23
  const trainers = loadDefaultJson("../trainer-data/trainerData.json");
24
24
  const trainersBack = loadDefaultJson("../trainer-data/trainerBackData.json");
25
+ const moves = loadDefaultJson("../move-data/moveData.json");
25
26
 
26
27
  export async function renderAllMons(rom, options = {}) {
27
28
  if (!rom || !(rom instanceof Uint8Array || Buffer.isBuffer(rom))) {
@@ -58,7 +59,6 @@ export async function renderAllIcons(rom, options = {}) {
58
59
  }
59
60
 
60
61
  const {
61
- assets: providedAssets = assets,
62
62
  icons: providedIcons = icons,
63
63
  outputDir = "./out",
64
64
  } = options;
@@ -69,7 +69,7 @@ export async function renderAllIcons(rom, options = {}) {
69
69
  const reader = new RomReader(rom, config);
70
70
 
71
71
  for (const itemName of Object.keys(providedIcons)) {
72
- await renderIcon(itemName, providedIcons, providedAssets, reader, rom, { outputDir });
72
+ await renderIcon(itemName, providedIcons, reader, rom, { outputDir });
73
73
  console.log(`Done: ${itemName}`);
74
74
  }
75
75
  }
@@ -100,6 +100,23 @@ export async function renderAllTrainers(rom, options = {}) {
100
100
  }
101
101
  }
102
102
 
103
+ export async function renderAllMoves(rom, options = {}) {
104
+ const {
105
+ moves: providedMoves = moves,
106
+ outputDir = "./out",
107
+ } = options;
108
+
109
+ fs.mkdirSync(outputDir, { recursive: true });
110
+
111
+ const config = getRomConfig(rom);
112
+ const reader = new RomReader(rom, config);
113
+
114
+ for (const moveName of Object.keys(providedMoves)) {
115
+ await renderMove(moveName, providedMoves, reader, rom, { outputDir });
116
+ console.log(`Done: ${moveName}`);
117
+ }
118
+ }
119
+
103
120
  export async function renderAllGraphics(rom, options = {}) { // eventually I will speed this up instead of doing it sequentially :p but for now its fine I guess
104
121
  if (!rom || !(rom instanceof Uint8Array || Buffer.isBuffer(rom))) {
105
122
  throw new TypeError("renderAllGraphics(rom, options) requires rom Buffer/Uint8Array as first argument");
@@ -109,6 +126,7 @@ export async function renderAllGraphics(rom, options = {}) { // eventually I wil
109
126
  outputMonDir = "./out/mons",
110
127
  outputIconDir = "./out/icons",
111
128
  outputTrainerDir = "./out/trainers",
129
+ outputMoveDir = "./out/moves"
112
130
  } = options;
113
131
 
114
132
  await renderAllMons(rom, {
@@ -125,6 +143,10 @@ export async function renderAllGraphics(rom, options = {}) { // eventually I wil
125
143
  outputDir: outputTrainerDir,
126
144
  trainerBackPics: true,
127
145
  });
146
+
147
+ await renderAllMoves(rom, {
148
+ outputDir: outputMoveDir,
149
+ })
128
150
  }
129
151
 
130
152
  export function loadDefaultRom() {
@@ -14,7 +14,7 @@ const streamToBuffer = (stream) => new Promise((resolve, reject) => {
14
14
  stream.on("error", reject);
15
15
  });
16
16
 
17
- export async function renderIcon(itemName, items, assets, reader, rom, options = {}) {
17
+ export async function renderIcon(itemName, items, reader, rom, options = {}) {
18
18
  const { outputDir = null } = options;
19
19
 
20
20
  if (!rom || !(rom instanceof Uint8Array || Buffer.isBuffer(rom))) {
@@ -0,0 +1,85 @@
1
+ // Copyright (c) 2026 chickenPoo
2
+ // Licensed under the MIT License. See LICENSE file in project root.
3
+
4
+ import { extract } from "../extract.js";
5
+ import { render4bppImage } from "../render-4bpp-image.js";
6
+ import { resolveMovePic } from "./resolvers/move-pic-resolver.js";
7
+ import { resolveMovePal } from "./resolvers/move-pal-resolver.js";
8
+ import fs from "fs";
9
+ import { PNG } from "pngjs";
10
+
11
+ // let's see if I remember how to throw one of these together...
12
+
13
+ const streamToBuffer = (stream) => new Promise((resolve, reject) => {
14
+ const chunks = [];
15
+ stream.on("data", chunk => chunks.push(chunk));
16
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
17
+ stream.on("error", reject);
18
+ });
19
+
20
+ const extractFrameFromImage = (imageData, fullWidth, frameData) => { // in theory should work...
21
+ const { x, y, width, height } = frameData;
22
+ const frameImage = new Uint8ClampedArray(width * height * 4);
23
+
24
+ for (let row = 0; row < height; row++) {
25
+ for (let col = 0; col < width; col++) {
26
+ const srcIndex = ((y + row) * fullWidth + (x + col)) * 4;
27
+ const dstIndex = (row * width + col) * 4;
28
+ frameImage[dstIndex] = imageData[srcIndex];
29
+ frameImage[dstIndex + 1] = imageData[srcIndex + 1];
30
+ frameImage[dstIndex + 2] = imageData[srcIndex + 2];
31
+ frameImage[dstIndex + 3] = imageData[srcIndex + 3];
32
+ }
33
+ }
34
+
35
+ return frameImage;
36
+ };
37
+
38
+ export async function renderMove(moveName, moves, reader, rom, options = {}) {
39
+ const {
40
+ outputDir = null
41
+ } = options;
42
+ if (!rom || !(rom instanceof Uint8Array || Buffer.isBuffer(rom))) {
43
+ throw new TypeError("renderMove(..., rom) requires a ROM Buffer/Uint8Array");
44
+ }
45
+
46
+ const move = moves[moveName];
47
+ if (!move) {
48
+ throw new Error(`Missing move: ${moveName}`)
49
+ }
50
+
51
+ const movePic = resolveMovePic(move, reader, moveName);
52
+ const movePal = resolveMovePal(move, reader, moveName);
53
+ if (!movePal || !movePic) {
54
+ throw new Error(`Missing assets for: ${moveName}`);
55
+ }
56
+
57
+ const moveImageData = extract(movePic, rom);
58
+ const rawMovePalData = extract(movePal, rom);
59
+ const width = move.imageWidth;
60
+ const height = move.imageHeight;
61
+
62
+ const image = render4bppImage({
63
+ tileData: moveImageData.data,
64
+ paletteData: rawMovePalData.data,
65
+ width,
66
+ height,
67
+ });
68
+
69
+ 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
70
+ const dir = `${outputDir}/${moveName}`;
71
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
72
+
73
+ for (let i = 0; i < move.frames.length; i++) {
74
+ const frame = move.frames[i];
75
+ const frameImageData = extractFrameFromImage(image, width, frame);
76
+
77
+ const png = new PNG({ width: frame.width, height: frame.height });
78
+ png.data = frameImageData;
79
+ const pngBuffer = await streamToBuffer(png.pack());
80
+
81
+ const fileName = `${dir}/frame-${i}.png`;
82
+ fs.writeFileSync(fileName, pngBuffer);
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,10 @@
1
+ export function resolveMovePal(move, reader, moveName) {
2
+ const table = reader.getTable("moveAnimPaletteTable");
3
+ const entryOffset = table + move.index * 8;
4
+ const ptr = reader.readOffset(entryOffset);
5
+ return {
6
+ name: `move_${moveName}_pal`,
7
+ offset: ptr,
8
+ size: 40 // maybe this will continue working? perhaps? please?
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ export function resolveMovePic(move, reader, moveName) {
2
+ const table = reader.getTable("moveAnimPicTable");
3
+ const entryOffset = table + move.index * 8;
4
+ const ptr = reader.readOffset(entryOffset);
5
+ return {
6
+ name: `move_${moveName}_pic`,
7
+ offset: ptr
8
+ // I don't think this needs a size... since I think most of these are all lz77 compressed... if not more JSON time :/
9
+ }
10
+ }
@@ -2,8 +2,15 @@ export function resolveTrainerBackPic(trainer, reader, trainerName) {
2
2
  const table = reader.getTable("trainerBackPicTable");
3
3
  const entryOffset = table + trainer.index * 8;
4
4
  const ptr = reader.readPointer(entryOffset);
5
+ let size;
6
+ if (trainerName === "RED" || trainerName === "LEAF") {
7
+ size = 10240;
8
+ } else {
9
+ size = 8192;
10
+ }
5
11
  return {
6
12
  name: `trainer_${trainerName}_back_pic`,
7
13
  offset: ptr,
14
+ size,
8
15
  }
9
16
  }
@@ -0,0 +1,169 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ // === CONFIG ===
5
+ const USE_PREBUILT_TEMPLATES = true; // Set to true to load from moveStructData.json instead of parsing .c files
6
+ // ===
7
+
8
+ // Paths
9
+ const movePicMap = path.resolve("../move_graphics/gBattleAnimPicTable.h");
10
+ const moveDataPath = "../move-data/moveData.json";
11
+ const unusedTagData = JSON.parse(fs.readFileSync(path.resolve("../move-data/unusedTags.json"), "utf-8"));
12
+
13
+ // Read animation pic table to get move animation tags
14
+ const movePicLines = fs.readFileSync(movePicMap, "utf-8").split("\n");
15
+
16
+ const moveData = {};
17
+ let moveIndex = 0;
18
+
19
+ // Tags that are defined but never assigned a SpriteTemplate
20
+ const unusedTags = new Set(Object.keys(unusedTagData));
21
+
22
+ // Parse gBattleAnimPicTable.h for animation tags
23
+ for (const line of movePicLines) {
24
+ const match = line.match(/\{\s*(gBattleAnimSpriteGfx_[A-Za-z0-9_]+)\s*,\s*(0x[0-9A-Fa-f]+)\s*,\s*(ANIM_TAG_[A-Za-z0-9_]+)\s*\},/);
25
+ if (match) {
26
+ const moveName = match[3].replace("ANIM_TAG_", "");
27
+ const moveTag = match[3];
28
+
29
+ if (!moveData[moveName]) {
30
+ moveData[moveName] = { index: moveIndex++ };
31
+ }
32
+ moveData[moveName].animTag = moveTag;
33
+
34
+ if (unusedTagData[moveTag]) {
35
+ Object.assign(moveData[moveName], unusedTagData[moveTag]);
36
+ moveData[moveName].unused = true;
37
+ }
38
+ }
39
+ }
40
+
41
+ // Parse SpriteTemplate structs from battle_anim_*.c files (or load prebuilt)
42
+ let templates = {};
43
+
44
+ if (USE_PREBUILT_TEMPLATES) {
45
+ // Load from prebuilt JSON
46
+ const prebuiltPath = "../move-data/moveStructData.json";
47
+ templates = JSON.parse(fs.readFileSync(prebuiltPath, "utf-8"));
48
+ console.log(`Loaded ${Object.keys(templates).length} templates from prebuilt file.`);
49
+ } else {
50
+ // Parse from source files
51
+ const animFiles = [
52
+ "../move_graphics/battle_anim_bug.c", "../move_graphics/battle_anim_dark.c",
53
+ "../move_graphics/battle_anim_dragon.c", "../move_graphics/battle_anim_effects_1.c",
54
+ "../move_graphics/battle_anim_effects_2.c", "../move_graphics/battle_anim_effects_3.c",
55
+ "../move_graphics/battle_anim_electric.c", "../move_graphics/battle_anim_fight.c",
56
+ "../move_graphics/battle_anim_fire.c", "../move_graphics/battle_anim_flying.c",
57
+ "../move_graphics/battle_anim_ghost.c", "../move_graphics/battle_anim_ground.c",
58
+ "../move_graphics/battle_anim_ice.c", "../move_graphics/battle_anim_normal.c",
59
+ "../move_graphics/battle_anim_poison.c", "../move_graphics/battle_anim_psychic.c",
60
+ "../move_graphics/battle_anim_rock.c", "../move_graphics/battle_anim_water.c",
61
+ "../move_graphics/battle_anim_status_effects.c",
62
+ ];
63
+
64
+ for (const file of animFiles) {
65
+ const lines = fs.readFileSync(file, "utf-8").split("\n");
66
+ Object.assign(templates, parseStructs(lines));
67
+ }
68
+ console.log(`Parsed ${Object.keys(templates).length} templates from source files.`);
69
+ }
70
+
71
+ // Build template lookup by tileTag
72
+ const templateByTag = Object.fromEntries(
73
+ Object.values(templates).map(t => [t.tileTag, t])
74
+ );
75
+
76
+ // Check for missing animTags
77
+ const animTagsInPicTable = Object.values(moveData).map(item => item.animTag);
78
+ const tileTagsInTemplates = Object.values(templates).map(t => t.tileTag);
79
+ const missingTags = animTagsInPicTable.filter(tag => !tileTagsInTemplates.includes(tag));
80
+
81
+ if (missingTags.length > 0) {
82
+ console.log("Missing animTags from templates:");
83
+ missingTags.forEach(tag => {
84
+ if (!unusedTags.has(tag)) console.log(tag);
85
+ });
86
+ } else {
87
+ console.log("No missing animTags.");
88
+ }
89
+
90
+ // Extract OAM size info for each move
91
+ for (const key in moveData) {
92
+ const item = moveData[key];
93
+ const template = templateByTag[item.animTag];
94
+
95
+ if (template?.oam) {
96
+ const size = extractOamSize(template.oam);
97
+ if (size) {
98
+ item.possibleFrameSize = size.raw;
99
+ item.possibleFrameWidth = size.width;
100
+ item.possibleFrameHeight = size.height;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Write final output
106
+ fs.writeFileSync(moveDataPath, JSON.stringify(moveData, null, 2));
107
+ console.log(`Wrote ${Object.keys(moveData).length} moves to ${moveDataPath}`);
108
+
109
+ // --- Helper Functions ---
110
+
111
+ function parseStructs(fileLines) {
112
+ const results = {};
113
+ let current = null;
114
+ let braceDepth = 0;
115
+ let inStruct = false;
116
+
117
+ for (const line of fileLines) {
118
+ const trimmed = line.trim();
119
+ const startMatch = trimmed.match(/^(static\s+)?const\s+struct\s+SpriteTemplate\s+(\w+)/);
120
+
121
+ if (startMatch) {
122
+ current = { name: startMatch[2] };
123
+ inStruct = true;
124
+ braceDepth = trimmed.includes("{") ? 1 : 0;
125
+ continue;
126
+ }
127
+
128
+ if (!inStruct) continue;
129
+
130
+ // Track brace depth
131
+ for (const char of trimmed) {
132
+ if (char === "{") braceDepth++;
133
+ if (char === "}") braceDepth--;
134
+ }
135
+
136
+ // Parse struct fields
137
+ if (trimmed.startsWith(".")) {
138
+ const eqIndex = trimmed.indexOf("=");
139
+ if (eqIndex === -1) continue;
140
+
141
+ const key = trimmed.slice(1, eqIndex).trim();
142
+ let value = trimmed.slice(eqIndex + 1).replace(",", "").trim();
143
+ if (value === "NULL") value = null;
144
+ else if (value.startsWith("&")) value = value.slice(1);
145
+
146
+ current[key] = value;
147
+ }
148
+
149
+ // End of struct
150
+ if (inStruct && braceDepth === 0) {
151
+ results[current.name] = current;
152
+ current = null;
153
+ inStruct = false;
154
+ }
155
+ }
156
+
157
+ return results;
158
+ }
159
+
160
+ function extractOamSize(oam) {
161
+ const match = oam?.match(/(\d+)x(\d+)$/);
162
+ if (!match) return null;
163
+
164
+ return {
165
+ width: parseInt(match[1], 10),
166
+ height: parseInt(match[2], 10),
167
+ raw: match[0]
168
+ };
169
+ }