silphscope 1.2.31 → 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/main.js +2 -2
- package/move-data/moveData.json +5940 -0
- package/move-data/moveStructData.json +3802 -0
- package/move-data/unusedTags.json +986 -0
- package/package.json +3 -2
- package/rom-configs/firered.js +2 -0
- package/src/graphics/graphics-extractor-main.js +25 -3
- package/src/graphics/icons/render-icons.js +1 -1
- package/src/graphics/moves/render-moves.js +85 -0
- package/src/graphics/moves/resolvers/move-pal-resolver.js +10 -0
- package/src/graphics/moves/resolvers/move-pic-resolver.js +10 -0
- package/src/move-data-parser.js +169 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silphscope",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/rom-configs/firered.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|