mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +00:00
feat(editor): add --diff-headers cross-file header comparison
Compares two .w* files at the standard catalog header level: 4-byte magic, version, catalog name, entry count, total file bytes. Useful for confirming a JSON round-trip didn't drift, checking whether two preset emissions produced equivalent output, or quickly diagnosing when a content snapshot has silently shifted (entry count up means content was added, file bytes up but everything else same means entry payloads got fatter). Output uses = / ≠ markers per field so visual scanning is fast. Three diagnostic summary cases: identical headers (and same bytes — possibly byte-equal, run cmp(1) to confirm), same shape but bytes differ (entry payloads diverged), and different formats entirely (files are unrelated). Returns exit 1 if any field differs, so the flag composes into shell pipelines (`if diff-headers a.wcms b.wcms; then ...`). World/asset formats stop after magic since their layouts diverge from the standard catalog header. Supports --json variant for tooling integration.
This commit is contained in:
parent
48984ca375
commit
99a952299b
6 changed files with 205 additions and 1 deletions
|
|
@ -1456,6 +1456,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_spell_schools_catalog.cpp
|
||||
tools/editor/cli_lfg_catalog.cpp
|
||||
tools/editor/cli_catalog_grep.cpp
|
||||
tools/editor/cli_diff_headers.cpp
|
||||
tools/editor/cli_macros_catalog.cpp
|
||||
tools/editor/cli_char_features_catalog.cpp
|
||||
tools/editor/cli_pvp_catalog.cpp
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ const char* const kArgRequired[] = {
|
|||
"--export-wliq-json", "--import-wliq-json",
|
||||
"--info-magic", "--summary-dir", "--rename-by-magic",
|
||||
"--bulk-rename-by-magic", "--touch-tree", "--tree-summary-md",
|
||||
"--catalog-grep",
|
||||
"--catalog-grep", "--diff-headers",
|
||||
"--gen-animations", "--gen-animations-combat", "--gen-animations-movement",
|
||||
"--info-wani", "--validate-wani",
|
||||
"--export-wani-json", "--import-wani-json",
|
||||
|
|
|
|||
188
tools/editor/cli_diff_headers.cpp
Normal file
188
tools/editor/cli_diff_headers.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#include "cli_diff_headers.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_format_table.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct HeaderInfo {
|
||||
fs::path path;
|
||||
char magic[4];
|
||||
bool magicOk = false;
|
||||
const FormatMagicEntry* fmt = nullptr;
|
||||
uint32_t version = 0;
|
||||
bool versionOk = false;
|
||||
std::string catalogName;
|
||||
bool catalogNameOk = false;
|
||||
uint32_t entryCount = 0;
|
||||
bool entryCountOk = false;
|
||||
uintmax_t fileBytes = 0;
|
||||
};
|
||||
|
||||
bool readHeader(const fs::path& path, HeaderInfo& out) {
|
||||
out.path = path;
|
||||
if (!fs::exists(path) || !fs::is_regular_file(path)) return false;
|
||||
out.fileBytes = fs::file_size(path);
|
||||
std::ifstream is(path, std::ios::binary);
|
||||
if (!is) return false;
|
||||
if (!is.read(out.magic, 4) || is.gcount() != 4) return false;
|
||||
out.magicOk = true;
|
||||
out.fmt = findFormatByMagic(out.magic);
|
||||
// Asset / world formats don't have the standard catalog
|
||||
// header, so stop after magic for those.
|
||||
if (!out.fmt || out.fmt->infoFlag == nullptr) return true;
|
||||
if (!is.read(reinterpret_cast<char*>(&out.version), 4)) return true;
|
||||
out.versionOk = true;
|
||||
uint32_t nameLen = 0;
|
||||
if (!is.read(reinterpret_cast<char*>(&nameLen), 4)) return true;
|
||||
if (nameLen > (1u << 20)) return true;
|
||||
out.catalogName.resize(nameLen);
|
||||
if (nameLen > 0) {
|
||||
if (!is.read(out.catalogName.data(), nameLen)) {
|
||||
out.catalogName.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
out.catalogNameOk = true;
|
||||
if (!is.read(reinterpret_cast<char*>(&out.entryCount), 4)) {
|
||||
return true;
|
||||
}
|
||||
out.entryCountOk = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* sameOrDiffMarker(bool same) {
|
||||
return same ? " =" : " ≠";
|
||||
}
|
||||
|
||||
int handleDiff(int& i, int argc, char** argv) {
|
||||
std::string fileA = argv[++i];
|
||||
if (i + 1 >= argc) {
|
||||
std::fprintf(stderr,
|
||||
"diff-headers: missing second file argument\n");
|
||||
return 1;
|
||||
}
|
||||
std::string fileB = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
HeaderInfo a, b;
|
||||
if (!readHeader(fileA, a)) {
|
||||
std::fprintf(stderr,
|
||||
"diff-headers: cannot read %s\n", fileA.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (!readHeader(fileB, b)) {
|
||||
std::fprintf(stderr,
|
||||
"diff-headers: cannot read %s\n", fileB.c_str());
|
||||
return 1;
|
||||
}
|
||||
bool magicSame = std::memcmp(a.magic, b.magic, 4) == 0;
|
||||
bool versionSame = magicSame && a.versionOk && b.versionOk &&
|
||||
a.version == b.version;
|
||||
bool nameSame = magicSame && a.catalogNameOk && b.catalogNameOk &&
|
||||
a.catalogName == b.catalogName;
|
||||
bool countSame = magicSame && a.entryCountOk && b.entryCountOk &&
|
||||
a.entryCount == b.entryCount;
|
||||
bool bytesSame = a.fileBytes == b.fileBytes;
|
||||
bool allSame = magicSame && versionSame && nameSame &&
|
||||
countSame && bytesSame;
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["fileA"] = fileA;
|
||||
j["fileB"] = fileB;
|
||||
j["magicSame"] = magicSame;
|
||||
j["versionSame"] = versionSame;
|
||||
j["catalogNameSame"] = nameSame;
|
||||
j["entryCountSame"] = countSame;
|
||||
j["bytesSame"] = bytesSame;
|
||||
j["identicalHeaders"] = allSame;
|
||||
char ma[5] = {a.magic[0], a.magic[1], a.magic[2], a.magic[3], 0};
|
||||
char mb[5] = {b.magic[0], b.magic[1], b.magic[2], b.magic[3], 0};
|
||||
j["a"] = {
|
||||
{"magic", ma},
|
||||
{"version", a.version},
|
||||
{"catalogName", a.catalogName},
|
||||
{"entryCount", a.entryCount},
|
||||
{"fileBytes", a.fileBytes},
|
||||
};
|
||||
j["b"] = {
|
||||
{"magic", mb},
|
||||
{"version", b.version},
|
||||
{"catalogName", b.catalogName},
|
||||
{"entryCount", b.entryCount},
|
||||
{"fileBytes", b.fileBytes},
|
||||
};
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return allSame ? 0 : 1;
|
||||
}
|
||||
char ma[5] = {a.magic[0], a.magic[1], a.magic[2], a.magic[3], 0};
|
||||
char mb[5] = {b.magic[0], b.magic[1], b.magic[2], b.magic[3], 0};
|
||||
std::printf("diff-headers:\n A: %s\n B: %s\n",
|
||||
fileA.c_str(), fileB.c_str());
|
||||
std::printf("\n");
|
||||
std::printf(" field A B\n");
|
||||
std::printf(" ---------- ------------------------ ------------------------\n");
|
||||
std::printf("%s magic '%s'%s '%s'\n",
|
||||
sameOrDiffMarker(magicSame), ma,
|
||||
(a.fmt ? "" : " (unknown)"), mb);
|
||||
if (magicSame && a.versionOk && b.versionOk) {
|
||||
std::printf("%s version %-24u %u\n",
|
||||
sameOrDiffMarker(versionSame),
|
||||
a.version, b.version);
|
||||
}
|
||||
if (magicSame && a.catalogNameOk && b.catalogNameOk) {
|
||||
std::printf("%s catalogName %-24s %s\n",
|
||||
sameOrDiffMarker(nameSame),
|
||||
a.catalogName.c_str(), b.catalogName.c_str());
|
||||
}
|
||||
if (magicSame && a.entryCountOk && b.entryCountOk) {
|
||||
std::printf("%s entryCount %-24u %u\n",
|
||||
sameOrDiffMarker(countSame),
|
||||
a.entryCount, b.entryCount);
|
||||
}
|
||||
std::printf("%s fileBytes %-24llu %llu\n",
|
||||
sameOrDiffMarker(bytesSame),
|
||||
static_cast<unsigned long long>(a.fileBytes),
|
||||
static_cast<unsigned long long>(b.fileBytes));
|
||||
std::printf("\n ");
|
||||
if (allSame) {
|
||||
std::printf("identical at the header level (and same byte size — "
|
||||
"possibly byte-equal, run cmp(1) to confirm)\n");
|
||||
} else if (magicSame && versionSame && nameSame && countSame &&
|
||||
!bytesSame) {
|
||||
std::printf("same format / version / name / entry count, "
|
||||
"but different byte sizes — entry payloads differ\n");
|
||||
} else if (!magicSame) {
|
||||
std::printf("DIFFERENT FORMATS — files are unrelated\n");
|
||||
} else {
|
||||
std::printf("header fields differ — see ≠ markers above\n");
|
||||
}
|
||||
return allSame ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleDiffHeaders(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--diff-headers") == 0 && i + 2 < argc) {
|
||||
outRc = handleDiff(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
11
tools/editor/cli_diff_headers.hpp
Normal file
11
tools/editor/cli_diff_headers.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleDiffHeaders(int& i, int argc, char** argv, int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
|
|
@ -93,6 +93,7 @@
|
|||
#include "cli_spell_schools_catalog.hpp"
|
||||
#include "cli_lfg_catalog.hpp"
|
||||
#include "cli_catalog_grep.hpp"
|
||||
#include "cli_diff_headers.hpp"
|
||||
#include "cli_macros_catalog.hpp"
|
||||
#include "cli_char_features_catalog.hpp"
|
||||
#include "cli_pvp_catalog.hpp"
|
||||
|
|
@ -237,6 +238,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleSpellSchoolsCatalog,
|
||||
handleLFGCatalog,
|
||||
handleCatalogGrep,
|
||||
handleDiffHeaders,
|
||||
handleMacrosCatalog,
|
||||
handleCharFeaturesCatalog,
|
||||
handlePVPCatalog,
|
||||
|
|
|
|||
|
|
@ -1363,6 +1363,8 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Emit a Markdown report of a content tree (per-format counts + per-file detail with catalog name + entry count). Stdout if no out path\n");
|
||||
std::printf(" --catalog-grep <pattern> <dir> [--case-sensitive] [--json]\n");
|
||||
std::printf(" Recursively search catalog NAMES (the internal name field) across .w* files in <dir>. Case-insensitive by default. Exit 1 if no match\n");
|
||||
std::printf(" --diff-headers <fileA> <fileB> [--json]\n");
|
||||
std::printf(" Compare two .w* files at the standard catalog header level (magic / version / name / entry count / file size). Exit 1 if any field differs\n");
|
||||
std::printf(" --gen-animations <wani-base> [name]\n");
|
||||
std::printf(" Emit .wani starter: 5 essential animations (Stand / Walk / Run / Death / AttackUnarmed) with fallback chains\n");
|
||||
std::printf(" --gen-animations-combat <wani-base> [name]\n");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue