feat(editor): add --diff-whm and --diff-woc completing the open-format diff suite

Last two missing entries in the diff family — terrain heightmap pairs
and collision meshes:

  wowee_editor --diff-whm Z1/Z1_30_30 Z2/Z2_31_30
  Diff: ...
                         a              b
    tile         : (  30,  30)    (  31,  30)  DIFF
    loadedChunks :          256          256
    doodadPlace  :            0            0
    wmoPlace     :            0            0
    heightRange  : [-1.50,1.50]  [-1.50,1.50]

  wowee_editor --diff-woc tile_a.woc tile_b.woc
  Diff: ...
                         a              b
    tile        : (  30,  30)    (  31,  30)  DIFF
    triangles   :        32768        32768
    walkable    :        32768        32768
    steep       :            0            0

--diff-whm: tile coord, loaded chunk count, doodad/WMO placement
counts, texture/name table sizes, height range (min/max with float
epsilon). Pointwise height compare intentionally not done — float
perturbation from format round-trips would false-flag.

--diff-woc: tile coord, total triangles, walkable + steep counts.
Catches whether a --regen-collision pass actually changed something.

Diff family is now exhaustively complete for every shippable open
format:
  --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
  --diff-whm    WHM/WOT terrain pair vs pair
  --diff-woc    WOC collision vs collision

Verified: tile (30,30) vs (31,30) reports tile DIFF + identical
counts (since both are flat scaffolds); same-vs-self reports
IDENTICAL with exit 0.
This commit is contained in:
Kelsi 2026-05-06 14:35:03 -07:00
parent cc3e85be5a
commit f8f5735d9b

View file

@ -614,6 +614,10 @@ static void printUsage(const char* argv0) {
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(" --diff-whm <a-base> <b-base> [--json]\n");
std::printf(" Compare two WHM/WOT terrain pairs (chunks, height range, placements)\n");
std::printf(" --diff-woc <a> <b> [--json]\n");
std::printf(" Compare two WOC collision meshes (triangles, walkable/steep counts, tile)\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");
@ -702,6 +706,16 @@ int main(int argc, char* argv[]) {
"--diff-wob requires <a-base> <b-base>\n");
return 1;
}
if (std::strcmp(argv[i], "--diff-whm") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--diff-whm requires <a-base> <b-base>\n");
return 1;
}
if (std::strcmp(argv[i], "--diff-woc") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--diff-woc requires <a.woc> <b.woc>\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;
@ -3284,6 +3298,158 @@ int main(int argc, char* argv[]) {
return 0;
}
return 1;
} else if (std::strcmp(argv[i], "--diff-whm") == 0 && i + 2 < argc) {
// Terrain diff. Catches the common "did my edit actually
// change anything?" question for heightmap tweaks. Compares
// chunk presence + height range + placement counts; not
// pointwise height compare since float perturbation from
// round-trips would false-flag.
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}) {
for (const char* ext : {".wot", ".whm"}) {
if (base->size() >= 4 && base->substr(base->size() - 4) == ext) {
*base = base->substr(0, base->size() - 4);
break;
}
}
}
for (const auto& base : {aBase, bBase}) {
if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) {
std::fprintf(stderr,
"diff-whm: WHM/WOT not found: %s.{whm,wot}\n", base.c_str());
return 1;
}
}
wowee::pipeline::ADTTerrain a, b;
wowee::pipeline::WoweeTerrainLoader::load(aBase, a);
wowee::pipeline::WoweeTerrainLoader::load(bBase, b);
// Per-side height range walk — same as --info-whm.
auto stats = [](const wowee::pipeline::ADTTerrain& t) {
struct S { int loaded; float minH, maxH; } s{0, 1e30f, -1e30f};
for (const auto& c : t.chunks) {
if (!c.heightMap.isLoaded()) continue;
s.loaded++;
for (float h : c.heightMap.heights) {
if (std::isfinite(h)) {
s.minH = std::min(s.minH, h);
s.maxH = std::max(s.maxH, h);
}
}
}
if (s.loaded == 0) { s.minH = 0; s.maxH = 0; }
return s;
};
auto sa = stats(a);
auto sb = stats(b);
struct Row { const char* label; long long av, bv; };
std::vector<Row> rows = {
{"loadedChunks", sa.loaded, sb.loaded},
{"doodadPlace", (long long)a.doodadPlacements.size(),(long long)b.doodadPlacements.size()},
{"wmoPlace", (long long)a.wmoPlacements.size(), (long long)b.wmoPlacements.size()},
{"textures", (long long)a.textures.size(), (long long)b.textures.size()},
{"doodadNames", (long long)a.doodadNames.size(), (long long)b.doodadNames.size()},
{"wmoNames", (long long)a.wmoNames.size(), (long long)b.wmoNames.size()},
};
int diffs = 0;
for (const auto& r : rows) if (r.av != r.bv) diffs++;
// Tile coords + height range comparison (epsilon for floats).
bool tileMatch = (a.coord.x == b.coord.x && a.coord.y == b.coord.y);
if (!tileMatch) diffs++;
bool heightMatch = (std::abs(sa.minH - sb.minH) < 0.01f &&
std::abs(sa.maxH - sb.maxH) < 0.01f);
if (!heightMatch) diffs++;
if (jsonOut) {
nlohmann::json j;
j["a"] = aBase; j["b"] = bBase;
for (const auto& r : rows) {
j[r.label] = {{"a", r.av}, {"b", r.bv}};
}
j["tile"] = {{"a", {a.coord.x, a.coord.y}},
{"b", {b.coord.x, b.coord.y}}};
j["heightRange"] = {{"a", {sa.minH, sa.maxH}},
{"b", {sb.minH, sb.maxH}}};
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 vs %s\n", aBase.c_str(), bBase.c_str());
std::printf(" a b\n");
std::printf(" %-13s: (%4d,%4d) (%4d,%4d) %s\n",
"tile", a.coord.x, a.coord.y, b.coord.x, b.coord.y,
tileMatch ? "" : "DIFF");
for (const auto& r : rows) {
std::printf(" %-13s: %12lld %12lld %s\n",
r.label, r.av, r.bv,
r.av == r.bv ? "" : "DIFF");
}
std::printf(" %-13s: [%.2f,%.2f] [%.2f,%.2f] %s\n",
"heightRange", sa.minH, sa.maxH, sb.minH, sb.maxH,
heightMatch ? "" : "DIFF");
if (diffs == 0) {
std::printf(" IDENTICAL\n");
return 0;
}
return 1;
} else if (std::strcmp(argv[i], "--diff-woc") == 0 && i + 2 < argc) {
// Collision-mesh diff. Confirms a --regen-collision pass
// actually changed something (or didn't, when the heightmap
// tweak was below the slope threshold).
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++;
for (const auto& p : {aPath, bPath}) {
if (!std::filesystem::exists(p)) {
std::fprintf(stderr, "diff-woc: WOC not found: %s\n", p.c_str());
return 1;
}
}
auto a = wowee::pipeline::WoweeCollisionBuilder::load(aPath);
auto b = wowee::pipeline::WoweeCollisionBuilder::load(bPath);
struct Row { const char* label; long long av, bv; };
std::vector<Row> rows = {
{"triangles", (long long)a.triangles.size(), (long long)b.triangles.size()},
{"walkable", (long long)a.walkableCount(), (long long)b.walkableCount()},
{"steep", (long long)a.steepCount(), (long long)b.steepCount()},
};
int diffs = 0;
for (const auto& r : rows) if (r.av != r.bv) diffs++;
bool tileMatch = (a.tileX == b.tileX && a.tileY == b.tileY);
if (!tileMatch) diffs++;
if (jsonOut) {
nlohmann::json j;
j["a"] = aPath; j["b"] = bPath;
for (const auto& r : rows) {
j[r.label] = {{"a", r.av}, {"b", r.bv}};
}
j["tile"] = {{"a", {a.tileX, a.tileY}},
{"b", {b.tileX, b.tileY}}};
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 vs %s\n", aPath.c_str(), bPath.c_str());
std::printf(" a b\n");
std::printf(" %-12s: (%4u,%4u) (%4u,%4u) %s\n",
"tile", a.tileX, a.tileY, b.tileX, b.tileY,
tileMatch ? "" : "DIFF");
for (const auto& r : rows) {
std::printf(" %-12s: %12lld %12lld %s\n",
r.label, r.av, r.bv,
r.av == r.bv ? "" : "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.