feat(editor): add --diff-wob completing the binary-format diff suite

Companion to --diff-wom for buildings. Same count-based shape so
round-trips through OBJ/glTF can be validated without false
positives from float perturbation:

  wowee_editor --diff-wob orig back

  Diff: orig.wob vs back.wob
                         a              b
    groups      :            5            5
    portals     :            3            3
    doodads     :           12           12
    materials   :            8            8
    groupTex    :            7            7
    totalVerts  :         4609         4609
    totalIdx    :        10950        10950
    name        : Stormwind     Stormwind
    boundRadius : match
    IDENTICAL

Compares: groups, portals, doodads, aggregated materials count
(per-group materials summed), aggregated group-texture count,
total verts/indices across all groups, name, boundRadius (with
0.01 epsilon).

Diff family is now complete across every binary format that ships
in a content pack:
  --diff-wcp    archive vs archive
  --diff-zone   unpacked zone dir vs zone dir
  --diff-glb    glTF binary vs glTF binary
  --diff-wom    WOM model vs WOM model
  --diff-wob    WOB building vs WOB building

Verified: identical pair reports IDENTICAL exit 0; pair with extra
group + extra doodad + name change reports 6 DIFFs with exit 1.
This commit is contained in:
Kelsi 2026-05-06 14:25:40 -07:00
parent a01b5e5e89
commit d618d6a517

View file

@ -608,6 +608,8 @@ static void printUsage(const char* argv0) {
std::printf(" Compare two glTF 2.0 binaries structurally; exit 0 if identical\n");
std::printf(" --diff-wom <a-base> <b-base> [--json]\n");
std::printf(" Compare two WOM models (verts, indices, bones, anims, batches, bounds)\n");
std::printf(" --diff-wob <a-base> <b-base> [--json]\n");
std::printf(" Compare two WOB buildings (groups, portals, doodads, totals)\n");
std::printf(" --pack-wcp <zone> [dst] Pack a zone dir/name into a .wcp archive and exit\n");
std::printf(" --unpack-wcp <wcp> [dst] Extract a WCP archive (default dst=custom_zones/) and exit\n");
std::printf(" --list-commands Print every recognized --flag, one per line, and exit\n");
@ -691,6 +693,11 @@ int main(int argc, char* argv[]) {
"--diff-wom requires <a-base> <b-base>\n");
return 1;
}
if (std::strcmp(argv[i], "--diff-wob") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--diff-wob requires <a-base> <b-base>\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;
@ -3172,6 +3179,107 @@ int main(int argc, char* argv[]) {
return 0;
}
return 1;
} else if (std::strcmp(argv[i], "--diff-wob") == 0 && i + 2 < argc) {
// Companion to --diff-wom for buildings. Same shape: count-
// based compare so round-trips through OBJ/glTF can be
// validated without false positives from float perturbation.
std::string aBase = argv[++i];
std::string bBase = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
for (auto* base : {&aBase, &bBase}) {
if (base->size() >= 4 &&
base->substr(base->size() - 4) == ".wob") {
*base = base->substr(0, base->size() - 4);
}
}
for (const auto& base : {aBase, bBase}) {
if (!wowee::pipeline::WoweeBuildingLoader::exists(base)) {
std::fprintf(stderr,
"diff-wob: WOB not found: %s.wob\n", base.c_str());
return 1;
}
}
auto a = wowee::pipeline::WoweeBuildingLoader::load(aBase);
auto b = wowee::pipeline::WoweeBuildingLoader::load(bBase);
// Aggregate vertex+index counts across all groups for the
// headline 'totalVerts/totalTris' metric (matches what
// --info-wob reports).
auto sumGroupVerts = [](const auto& bld) {
size_t s = 0;
for (const auto& g : bld.groups) s += g.vertices.size();
return s;
};
auto sumGroupIdx = [](const auto& bld) {
size_t s = 0;
for (const auto& g : bld.groups) s += g.indices.size();
return s;
};
struct Row {
const char* label;
long long av, bv;
};
// WoweeBuilding doesn't have a top-level textures vector or
// doodadSets — materials and textures are per-group, doodad
// sets are flattened. Aggregate the per-group counts.
long long aMats = 0, bMats = 0;
long long aGroupTex = 0, bGroupTex = 0;
for (const auto& g : a.groups) {
aMats += static_cast<long long>(g.materials.size());
aGroupTex += static_cast<long long>(g.texturePaths.size());
}
for (const auto& g : b.groups) {
bMats += static_cast<long long>(g.materials.size());
bGroupTex += static_cast<long long>(g.texturePaths.size());
}
std::vector<Row> rows = {
{"groups", (long long)a.groups.size(), (long long)b.groups.size()},
{"portals", (long long)a.portals.size(), (long long)b.portals.size()},
{"doodads", (long long)a.doodads.size(), (long long)b.doodads.size()},
{"materials", aMats, bMats},
{"groupTex", aGroupTex, bGroupTex},
{"totalVerts", (long long)sumGroupVerts(a), (long long)sumGroupVerts(b)},
{"totalIdx", (long long)sumGroupIdx(a), (long long)sumGroupIdx(b)},
};
int diffs = 0;
for (const auto& r : rows) if (r.av != r.bv) diffs++;
bool nameMatch = (a.name == b.name);
if (!nameMatch) diffs++;
bool radMatch = (std::abs(a.boundRadius - b.boundRadius) < 0.01f);
if (!radMatch) diffs++;
if (jsonOut) {
nlohmann::json j;
j["a"] = aBase + ".wob";
j["b"] = bBase + ".wob";
for (const auto& r : rows) {
j[r.label] = {{"a", r.av}, {"b", r.bv}};
}
j["name"] = {{"a", a.name}, {"b", b.name}};
j["boundRadiusMatch"] = radMatch;
j["totalDiffs"] = diffs;
j["identical"] = (diffs == 0);
std::printf("%s\n", j.dump(2).c_str());
return diffs == 0 ? 0 : 1;
}
std::printf("Diff: %s.wob vs %s.wob\n", aBase.c_str(), bBase.c_str());
std::printf(" a b\n");
for (const auto& r : rows) {
std::printf(" %-12s: %12lld %12lld %s\n",
r.label, r.av, r.bv,
r.av == r.bv ? "" : "DIFF");
}
std::printf(" %-12s: %-13s %-13s %s\n",
"name", a.name.substr(0, 13).c_str(),
b.name.substr(0, 13).c_str(),
nameMatch ? "" : "DIFF");
std::printf(" %-12s: %s\n", "boundRadius",
radMatch ? "match" : "DIFF");
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.