From 705899d6a772ff84eb1c996296031044bb90a9d0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:16:01 -0700 Subject: [PATCH] feat(editor): add --info-png and --info-jsondbc sidecar inspectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PNG (BLP sidecar) and JSON DBC (DBC sidecar) didn't have inspectors — the existing --info-* suite only covered the binary native formats (WOM/WOB/WOC/WOT/WHM/WCP). These fill the gap so debugging the asset_extract --emit-* output doesn't need GIMP / a JSON pretty-printer: wowee_editor --info-png Textures/Sky01.png PNG: Textures/Sky01.png size : 256 x 256 bit depth : 8 color : rgba (4 channels) file bytes: 142336 wowee_editor --info-jsondbc db/Spell.json JSON DBC: db/Spell.json format : wowee-jsondbc-1 source : Spell.dbc records : 47882 (header) / 47882 (actual) fields : 234 PNG inspector reads only the IHDR chunk (24 bytes total) — no pixel decode — so it works instantly on huge files. Validates the 8-byte signature, parses big-endian width/height, derives channel count from PNG color type (grayscale/rgb/palette/grayscale+alpha/ rgba per spec table 11.1). JSON DBC inspector parses with nlohmann::json, reports the schema fields (format/source/recordCount/fieldCount), and cross-checks recordCount against actual records[] array length. Exits 1 on count mismatch — catches truncated extracts where the header lies about how much data follows. Verified with hand-rolled 2x3 RGBA PNG (correct dims) and JSON DBC files (one matched, one mismatched 99 vs 1). --- tools/editor/main.cpp | 137 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 41931dbd..18c9848b 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -455,6 +455,10 @@ static void printUsage(const char* argv0) { std::printf(" Print WOT/WHM terrain metadata (tile, chunks, height range) and exit\n"); std::printf(" --info-extract [--json]\n"); std::printf(" Walk extracted asset tree and report open-format coverage and exit\n"); + std::printf(" --info-png [--json]\n"); + std::printf(" Print PNG header (width, height, channels, bit depth) and exit\n"); + std::printf(" --info-jsondbc [--json]\n"); + std::printf(" Print JSON DBC sidecar metadata (records, fields, source) and exit\n"); std::printf(" --list-missing-sidecars [--json]\n"); std::printf(" List proprietary files lacking open-format sidecars (one per line)\n"); std::printf(" --info-zone [--json]\n"); @@ -496,6 +500,7 @@ int main(int argc, char* argv[]) { "--data", "--info", "--info-wob", "--info-woc", "--info-wot", "--info-creatures", "--info-objects", "--info-quests", "--info-extract", "--list-missing-sidecars", + "--info-png", "--info-jsondbc", "--info-zone", "--info-wcp", "--list-wcp", "--list-creatures", "--list-objects", "--list-quests", "--unpack-wcp", "--pack-wcp", @@ -957,6 +962,138 @@ int main(int argc, char* argv[]) { total, missingPng.size(), missingJson.size(), missingWom.size(), missingWob.size(), missingWhm.size()); return total == 0 ? 0 : 1; + } else if (std::strcmp(argv[i], "--info-png") == 0 && i + 1 < argc) { + // Inspect a PNG sidecar — width, height, channels, bit depth. + // Reads only the IHDR chunk (16 bytes after the 8-byte + // signature) so it works on huge files instantly without + // decoding pixels. Useful for verifying that the BLP→PNG + // emitter produced the expected dimensions. + std::string path = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + std::ifstream in(path, std::ios::binary); + if (!in) { + std::fprintf(stderr, "info-png: cannot open %s\n", path.c_str()); + return 1; + } + uint8_t buf[24]; + in.read(reinterpret_cast(buf), 24); + if (!in || in.gcount() < 24) { + std::fprintf(stderr, "info-png: %s too short to be a PNG\n", path.c_str()); + return 1; + } + // Validate the 8-byte PNG signature: 89 50 4E 47 0D 0A 1A 0A + static const uint8_t kSig[8] = {0x89, 0x50, 0x4E, 0x47, + 0x0D, 0x0A, 0x1A, 0x0A}; + if (std::memcmp(buf, kSig, 8) != 0) { + std::fprintf(stderr, "info-png: %s missing PNG signature\n", path.c_str()); + return 1; + } + // IHDR chunk follows: 4-byte length, 4-byte type ('IHDR'), + // then 13-byte payload (width:4, height:4, bitDepth:1, + // colorType:1, compression:1, filter:1, interlace:1). + // All multi-byte ints in PNG are big-endian. + auto be32 = [](const uint8_t* p) { + return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | + (uint32_t(p[2]) << 8) | uint32_t(p[3]); + }; + uint32_t width = be32(buf + 16); + uint32_t height = be32(buf + 20); + // Need bit depth + color type — read the next 5 bytes. + uint8_t extra[5]; + in.read(reinterpret_cast(extra), 5); + uint8_t bitDepth = extra[0]; + uint8_t colorType = extra[1]; + // Channel count derives from color type (PNG spec table 11.1). + int channels = 0; + const char* colorName = "?"; + switch (colorType) { + case 0: channels = 1; colorName = "grayscale"; break; + case 2: channels = 3; colorName = "rgb"; break; + case 3: channels = 1; colorName = "palette"; break; + case 4: channels = 2; colorName = "grayscale+alpha"; break; + case 6: channels = 4; colorName = "rgba"; break; + } + // File size for a quick sanity check — a 1024x1024 RGBA PNG + // shouldn't be 12 bytes, that would mean truncation. + std::error_code ec; + uint64_t fsz = std::filesystem::file_size(path, ec); + if (jsonOut) { + nlohmann::json j; + j["png"] = path; + j["width"] = width; + j["height"] = height; + j["bitDepth"] = bitDepth; + j["channels"] = channels; + j["colorType"] = colorType; + j["colorTypeName"] = colorName; + j["fileSize"] = fsz; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("PNG: %s\n", path.c_str()); + std::printf(" size : %u x %u\n", width, height); + std::printf(" bit depth : %u\n", bitDepth); + std::printf(" color : %s (%d channel%s)\n", + colorName, channels, channels == 1 ? "" : "s"); + std::printf(" file bytes: %llu\n", static_cast(fsz)); + return 0; + } else if (std::strcmp(argv[i], "--info-jsondbc") == 0 && i + 1 < argc) { + // Inspect a JSON DBC sidecar (the JSON output of asset_extract + // --emit-json-dbc). Reports recordCount, fieldCount, source + // filename, and format version — useful for verifying the + // sidecar tracks the proprietary file's row count. + std::string path = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + std::ifstream in(path); + if (!in) { + std::fprintf(stderr, "info-jsondbc: cannot open %s\n", path.c_str()); + return 1; + } + nlohmann::json doc; + try { + in >> doc; + } catch (const std::exception& e) { + std::fprintf(stderr, "info-jsondbc: bad JSON in %s (%s)\n", + path.c_str(), e.what()); + return 1; + } + // The wowee JSON DBC schema (from open_format_emitter.cpp): + // {format, source, recordCount, fieldCount, records:[[...], ...]}. + // Tolerate missing fields rather than crashing — old sidecars + // may predate a field addition. + std::string format = doc.value("format", std::string{}); + std::string source = doc.value("source", std::string{}); + uint32_t recordCount = doc.value("recordCount", 0u); + uint32_t fieldCount = doc.value("fieldCount", 0u); + uint32_t actualRecs = 0; + if (doc.contains("records") && doc["records"].is_array()) { + actualRecs = static_cast(doc["records"].size()); + } + bool countMismatch = (recordCount != actualRecs); + if (jsonOut) { + nlohmann::json j; + j["jsondbc"] = path; + j["format"] = format; + j["source"] = source; + j["recordCount"] = recordCount; + j["fieldCount"] = fieldCount; + j["actualRecords"] = actualRecs; + j["countMismatch"] = countMismatch; + std::printf("%s\n", j.dump(2).c_str()); + return countMismatch ? 1 : 0; + } + std::printf("JSON DBC: %s\n", path.c_str()); + std::printf(" format : %s\n", format.empty() ? "?" : format.c_str()); + std::printf(" source : %s\n", source.empty() ? "?" : source.c_str()); + std::printf(" records : %u (header) / %u (actual)%s\n", + recordCount, actualRecs, + countMismatch ? " [MISMATCH]" : ""); + std::printf(" fields : %u\n", fieldCount); + return countMismatch ? 1 : 0; } else if (std::strcmp(argv[i], "--info-zone") == 0 && i + 1 < argc) { // Parse a zone.json and print every manifest field. Useful when // diffing two zones or auditing the audio/flag setup before