From d82f90dd82431c715e388f4ed52f5a4508b05f36 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 13:57:25 -0700 Subject: [PATCH] feat(editor): add --diff-glb completing the diff-* family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structural compare of two glTF 2.0 binaries. Completes the diff suite alongside --diff-wcp (archive-vs-archive) and --diff-zone (unpacked-zone-vs-zone): wowee_editor --diff-glb a.glb b.glb Diff: a.glb vs b.glb a b meshes : 1 2 DIFF primitives : 256 2 DIFF accessors : 258 4 DIFF bufferViews : 3 3 buffers : 1 1 BIN bytes : 890880 1781760 DIFF Reports per-category counts side-by-side with a 'DIFF' marker on mismatches. Compares structure (mesh/primitive/accessor counts + BIN chunk size), NOT byte-level — JSON key ordering can vary between tools so a byte diff would have false positives. JSON mode emits the per-field {a, b} pair plus totalDiffs + identical bool for CI consumption. Useful for confirming alternate export paths produce equivalent output (does --bake-zone-glb match concatenated --export-whm-glbs? does a tool refactor preserve the same shape?). Verified: same file vs itself reports IDENTICAL with exit 0. Single-tile WHM .glb (1 mesh, 256 primitives, 890KB BIN) vs 2-tile bake (2 meshes, 2 primitives, 1.7MB BIN): correctly flags 4 DIFFs with exit 1. --- tools/editor/main.cpp | 129 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 7a0aecff..e566bc91 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -585,6 +585,8 @@ static void printUsage(const char* argv0) { std::printf(" Compare two WCPs file-by-file; exit 0 if identical, 1 otherwise\n"); std::printf(" --diff-zone [--json]\n"); std::printf(" Compare two zone dirs (creatures/objects/quests/manifest); exit 0 if identical\n"); + std::printf(" --diff-glb [--json]\n"); + std::printf(" Compare two glTF 2.0 binaries structurally; exit 0 if identical\n"); std::printf(" --pack-wcp [dst] Pack a zone dir/name into a .wcp archive and exit\n"); std::printf(" --unpack-wcp [dst] Extract a WCP archive (default dst=custom_zones/) and exit\n"); std::printf(" --version Show version and format info\n\n"); @@ -652,6 +654,11 @@ int main(int argc, char* argv[]) { "--diff-zone requires \n"); return 1; } + if (std::strcmp(argv[i], "--diff-glb") == 0 && i + 2 >= argc) { + std::fprintf(stderr, + "--diff-glb requires \n"); + return 1; + } if (std::strcmp(argv[i], "--diff-wcp") == 0 && i + 2 >= argc) { std::fprintf(stderr, "--diff-wcp requires two paths\n"); return 1; @@ -2677,6 +2684,128 @@ int main(int argc, char* argv[]) { for (const auto& s : questOnlyA) std::printf(" - %s\n", s.c_str()); for (const auto& s : questOnlyB) std::printf(" + %s\n", s.c_str()); return 1; + } else if (std::strcmp(argv[i], "--diff-glb") == 0 && i + 2 < argc) { + // Structural compare of two .glb files. Useful for confirming + // that an alternate export path produces equivalent output + // (e.g. --bake-zone-glb vs concatenated --export-whm-glbs) + // or that a re-export of the same source is byte-equivalent. + // Compares structure (mesh/primitive/accessor counts + + // chunk sizes), NOT byte-level — JSON key ordering can vary. + std::string aPath = argv[++i]; + std::string bPath = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + // Reuse the parser from --info-glb. Inline here since it's + // small and the alternative is a 3-way handler refactor. + auto loadGlb = [](const std::string& path, + uint32_t& outJsonLen, uint32_t& outBinLen, + std::string& outJsonStr) -> bool { + std::ifstream in(path, std::ios::binary); + if (!in) return false; + std::vector bytes((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + if (bytes.size() < 20) return false; + uint32_t magic, version, totalLen; + std::memcpy(&magic, &bytes[0], 4); + std::memcpy(&version, &bytes[4], 4); + std::memcpy(&totalLen, &bytes[8], 4); + if (magic != 0x46546C67 || version != 2) return false; + std::memcpy(&outJsonLen, &bytes[12], 4); + if (20 + outJsonLen > bytes.size()) return false; + outJsonStr.assign(bytes.begin() + 20, + bytes.begin() + 20 + outJsonLen); + size_t binOff = 20 + outJsonLen; + if (binOff + 8 <= bytes.size()) { + std::memcpy(&outBinLen, &bytes[binOff], 4); + } else { + outBinLen = 0; + } + return true; + }; + uint32_t aJsonLen = 0, aBinLen = 0; + uint32_t bJsonLen = 0, bBinLen = 0; + std::string aJsonStr, bJsonStr; + if (!loadGlb(aPath, aJsonLen, aBinLen, aJsonStr)) { + std::fprintf(stderr, "diff-glb: failed to read %s\n", aPath.c_str()); + return 1; + } + if (!loadGlb(bPath, bJsonLen, bBinLen, bJsonStr)) { + std::fprintf(stderr, "diff-glb: failed to read %s\n", bPath.c_str()); + return 1; + } + // Pull structural counts from JSON. Skip if parse fails on + // either side — diff is meaningless then. + auto countOf = [](const nlohmann::json& j, const char* key) { + if (j.contains(key) && j[key].is_array()) { + return static_cast(j[key].size()); + } + return 0; + }; + int aMesh = 0, aPrim = 0, aAcc = 0, aBV = 0, aBuf = 0; + int bMesh = 0, bPrim = 0, bAcc = 0, bBV = 0, bBuf = 0; + try { + auto aj = nlohmann::json::parse(aJsonStr); + auto bj = nlohmann::json::parse(bJsonStr); + aMesh = countOf(aj, "meshes"); + bMesh = countOf(bj, "meshes"); + if (aj.contains("meshes") && aj["meshes"].is_array()) { + for (const auto& m : aj["meshes"]) { + if (m.contains("primitives") && m["primitives"].is_array()) { + aPrim += static_cast(m["primitives"].size()); + } + } + } + if (bj.contains("meshes") && bj["meshes"].is_array()) { + for (const auto& m : bj["meshes"]) { + if (m.contains("primitives") && m["primitives"].is_array()) { + bPrim += static_cast(m["primitives"].size()); + } + } + } + aAcc = countOf(aj, "accessors"); bAcc = countOf(bj, "accessors"); + aBV = countOf(aj, "bufferViews"); bBV = countOf(bj, "bufferViews"); + aBuf = countOf(aj, "buffers"); bBuf = countOf(bj, "buffers"); + } catch (const std::exception&) { + std::fprintf(stderr, "diff-glb: JSON parse failed on one side\n"); + return 1; + } + int diffs = (aMesh != bMesh) + (aPrim != bPrim) + (aAcc != bAcc) + + (aBV != bBV) + (aBuf != bBuf) + + (aBinLen != bBinLen); + if (jsonOut) { + nlohmann::json j; + j["a"] = aPath; j["b"] = bPath; + j["meshes"] = {{"a", aMesh}, {"b", bMesh}}; + j["primitives"] = {{"a", aPrim}, {"b", bPrim}}; + j["accessors"] = {{"a", aAcc}, {"b", bAcc}}; + j["bufferViews"] = {{"a", aBV}, {"b", bBV}}; + j["buffers"] = {{"a", aBuf}, {"b", bBuf}}; + j["binBytes"] = {{"a", aBinLen},{"b", bBinLen}}; + j["jsonBytes"] = {{"a", aJsonLen},{"b", bJsonLen}}; + j["totalDiffs"] = diffs; + j["identical"] = (diffs == 0); + std::printf("%s\n", j.dump(2).c_str()); + return diffs == 0 ? 0 : 1; + } + auto cmp = [](const char* name, int a, int b) { + std::printf(" %-12s: %6d %6d %s\n", name, a, b, + a == b ? "" : "DIFF"); + }; + std::printf("Diff: %s vs %s\n", aPath.c_str(), bPath.c_str()); + std::printf(" a b\n"); + cmp("meshes", aMesh, bMesh); + cmp("primitives", aPrim, bPrim); + cmp("accessors", aAcc, bAcc); + cmp("bufferViews", aBV, bBV); + cmp("buffers", aBuf, bBuf); + cmp("BIN bytes", static_cast(aBinLen), + static_cast(bBinLen)); + if (diffs == 0) { + std::printf(" IDENTICAL\n"); + return 0; + } + return 1; } else if (std::strcmp(argv[i], "--list-wcp") == 0 && i + 1 < argc) { // Like --info-wcp but prints every file path. Useful for spotting // missing or unexpected entries before unpacking.