diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c5ef73c..1d6bed96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 11afa072..fae77d7e 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.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", diff --git a/tools/editor/cli_diff_headers.cpp b/tools/editor/cli_diff_headers.cpp new file mode 100644 index 00000000..ec240436 --- /dev/null +++ b/tools/editor/cli_diff_headers.cpp @@ -0,0 +1,188 @@ +#include "cli_diff_headers.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +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(&out.version), 4)) return true; + out.versionOk = true; + uint32_t nameLen = 0; + if (!is.read(reinterpret_cast(&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(&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(a.fileBytes), + static_cast(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 diff --git a/tools/editor/cli_diff_headers.hpp b/tools/editor/cli_diff_headers.hpp new file mode 100644 index 00000000..70dcbeff --- /dev/null +++ b/tools/editor/cli_diff_headers.hpp @@ -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 diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index f8cfaa9e..8763a848 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -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, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index ef532625..b5eda1fb 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -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 [--case-sensitive] [--json]\n"); std::printf(" Recursively search catalog NAMES (the internal name field) across .w* files in . Case-insensitive by default. Exit 1 if no match\n"); + std::printf(" --diff-headers [--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 [name]\n"); std::printf(" Emit .wani starter: 5 essential animations (Stand / Walk / Run / Death / AttackUnarmed) with fallback chains\n"); std::printf(" --gen-animations-combat [name]\n");