mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
feat(editor): add --tree-summary-md content inventory generator
Walks a directory recursively, identifies every Wowee open-format file by 4-byte magic, parses the standard catalog header, and emits a Markdown report. Useful for content-bundle distributions to ship with a README of what's inside, and for change-log generation when diffing two content snapshots manually. The report has three sections: a summary table (total files / recognized / bytes), a per-format breakdown (magic / ext / file count / total entries / description), and a per-file detail table (path / magic / version / catalog name / entries / bytes). Output to stdout if no out path is given, otherwise written to a file. Reuses cli_format_table.cpp so any new format added in the future appears automatically without touching this tool.
This commit is contained in:
parent
386afcb4ef
commit
23bb97651c
6 changed files with 213 additions and 1 deletions
|
|
@ -1440,6 +1440,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_companions_catalog.cpp
|
||||
tools/editor/cli_spell_mechanics_catalog.cpp
|
||||
tools/editor/cli_keybindings_catalog.cpp
|
||||
tools/editor/cli_tree_summary_md.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ const char* const kArgRequired[] = {
|
|||
"--info-wliq", "--validate-wliq",
|
||||
"--export-wliq-json", "--import-wliq-json",
|
||||
"--info-magic", "--summary-dir", "--rename-by-magic",
|
||||
"--bulk-rename-by-magic", "--touch-tree",
|
||||
"--bulk-rename-by-magic", "--touch-tree", "--tree-summary-md",
|
||||
"--gen-animations", "--gen-animations-combat", "--gen-animations-movement",
|
||||
"--info-wani", "--validate-wani",
|
||||
"--export-wani-json", "--import-wani-json",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@
|
|||
#include "cli_companions_catalog.hpp"
|
||||
#include "cli_spell_mechanics_catalog.hpp"
|
||||
#include "cli_keybindings_catalog.hpp"
|
||||
#include "cli_tree_summary_md.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -219,6 +220,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleCompanionsCatalog,
|
||||
handleSpellMechanicsCatalog,
|
||||
handleKeybindingsCatalog,
|
||||
handleTreeSummaryMd,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
|
|
@ -1359,6 +1359,8 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Apply --rename-by-magic recursively to every file in <dir>. Conflicts are skipped without --force; exits 1 if any rename failed\n");
|
||||
std::printf(" --touch-tree <dir> [--json] [--quiet]\n");
|
||||
std::printf(" CI integrity check: open every recognized .w* file in <dir>, parse standard header, report PASS/FAIL + extension mismatches. Exit 1 on any failure\n");
|
||||
std::printf(" --tree-summary-md <dir> [out.md]\n");
|
||||
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(" --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");
|
||||
|
|
|
|||
196
tools/editor/cli_tree_summary_md.cpp
Normal file
196
tools/editor/cli_tree_summary_md.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#include "cli_tree_summary_md.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_format_table.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct FileRow {
|
||||
fs::path relPath;
|
||||
const FormatMagicEntry* fmt;
|
||||
uint32_t version;
|
||||
uint32_t entryCount;
|
||||
std::string catalogName;
|
||||
bool hasHeader;
|
||||
uintmax_t bytes;
|
||||
};
|
||||
|
||||
// Read the standard catalog header (magic+version+name+
|
||||
// entryCount). World/asset formats don't have this layout —
|
||||
// for those we leave the header fields blank.
|
||||
bool peekHeader(const fs::path& path, char magic[4],
|
||||
uint32_t& version, std::string& catalogName,
|
||||
uint32_t& entryCount) {
|
||||
std::ifstream is(path, std::ios::binary);
|
||||
if (!is) return false;
|
||||
if (!is.read(magic, 4) || is.gcount() != 4) return false;
|
||||
if (!is.read(reinterpret_cast<char*>(&version), 4)) return false;
|
||||
uint32_t nameLen = 0;
|
||||
if (!is.read(reinterpret_cast<char*>(&nameLen), 4)) return false;
|
||||
if (nameLen > (1u << 20)) return false;
|
||||
catalogName.resize(nameLen);
|
||||
if (nameLen > 0) {
|
||||
if (!is.read(catalogName.data(), nameLen)) return false;
|
||||
}
|
||||
if (!is.read(reinterpret_cast<char*>(&entryCount), 4)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int handleSummary(int& i, int argc, char** argv) {
|
||||
std::string dir = argv[++i];
|
||||
std::string outPath;
|
||||
if (parseOptArg(i, argc, argv)) outPath = argv[++i];
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
||||
std::fprintf(stderr,
|
||||
"tree-summary-md: not a directory: %s\n", dir.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::vector<FileRow> rows;
|
||||
std::map<std::string, uint64_t> formatFileCounts;
|
||||
std::map<std::string, uint64_t> formatEntryCounts;
|
||||
uint64_t totalFiles = 0;
|
||||
uint64_t totalRecognized = 0;
|
||||
uint64_t totalBytes = 0;
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
++totalFiles;
|
||||
char magic[4];
|
||||
std::ifstream is(entry.path(), std::ios::binary);
|
||||
if (!is.read(magic, 4) || is.gcount() != 4) continue;
|
||||
const FormatMagicEntry* fmt = findFormatByMagic(magic);
|
||||
if (!fmt) continue;
|
||||
++totalRecognized;
|
||||
FileRow r;
|
||||
r.relPath = fs::relative(entry.path(), dir);
|
||||
r.fmt = fmt;
|
||||
r.version = 0; r.entryCount = 0;
|
||||
r.bytes = entry.file_size();
|
||||
// Re-open and parse standard header for catalog formats.
|
||||
// Skip for world/asset formats (infoFlag == nullptr).
|
||||
if (fmt->infoFlag != nullptr) {
|
||||
char headerMagic[4];
|
||||
uint32_t v = 0, ec = 0;
|
||||
std::string cn;
|
||||
if (peekHeader(entry.path(), headerMagic, v, cn, ec)) {
|
||||
r.version = v; r.catalogName = cn;
|
||||
r.entryCount = ec; r.hasHeader = true;
|
||||
formatEntryCounts[fmt->magic] += ec;
|
||||
} else {
|
||||
r.hasHeader = false;
|
||||
}
|
||||
} else {
|
||||
r.hasHeader = false;
|
||||
}
|
||||
formatFileCounts[fmt->magic] += 1;
|
||||
totalBytes += r.bytes;
|
||||
rows.push_back(std::move(r));
|
||||
}
|
||||
// Build the markdown.
|
||||
std::ostringstream md;
|
||||
md << "# Wowee open-format inventory: " << dir << "\n\n";
|
||||
md << "Generated by `--tree-summary-md`. "
|
||||
<< "Walks the directory recursively and identifies every "
|
||||
<< "Wowee open-format file by its 4-byte magic.\n\n";
|
||||
md << "## Summary\n\n";
|
||||
md << "| Stat | Value |\n";
|
||||
md << "|------|-------|\n";
|
||||
md << "| Total files scanned | " << totalFiles << " |\n";
|
||||
md << "| Recognized .w* files | " << totalRecognized << " |\n";
|
||||
md << "| Unrecognized files | "
|
||||
<< (totalFiles - totalRecognized) << " |\n";
|
||||
md << "| Total recognized bytes | " << totalBytes << " |\n\n";
|
||||
if (!formatFileCounts.empty()) {
|
||||
md << "## Per-format breakdown\n\n";
|
||||
md << "| Magic | Extension | Files | Total entries | Description |\n";
|
||||
md << "|-------|-----------|-------|---------------|-------------|\n";
|
||||
for (const auto& kv : formatFileCounts) {
|
||||
std::string magicStr = kv.first;
|
||||
const FormatMagicEntry* fmt = nullptr;
|
||||
for (const FormatMagicEntry* p = formatTableBegin();
|
||||
p != formatTableEnd(); ++p) {
|
||||
if (std::memcmp(p->magic, magicStr.data(), 4) == 0) {
|
||||
fmt = p; break;
|
||||
}
|
||||
}
|
||||
uint64_t entries = formatEntryCounts.count(magicStr)
|
||||
? formatEntryCounts[magicStr] : 0;
|
||||
md << "| `" << magicStr << "` | `"
|
||||
<< (fmt ? fmt->extension : "?") << "` | "
|
||||
<< kv.second << " | " << entries << " | "
|
||||
<< (fmt ? fmt->description : "?") << " |\n";
|
||||
}
|
||||
md << "\n";
|
||||
}
|
||||
if (!rows.empty()) {
|
||||
md << "## Per-file detail\n\n";
|
||||
md << "| Path | Magic | Version | Catalog name | Entries | Bytes |\n";
|
||||
md << "|------|-------|---------|--------------|---------|-------|\n";
|
||||
for (const auto& r : rows) {
|
||||
char ms[5] = {r.fmt->magic[0], r.fmt->magic[1],
|
||||
r.fmt->magic[2], r.fmt->magic[3], 0};
|
||||
md << "| `" << r.relPath.string() << "` | `"
|
||||
<< ms << "` | ";
|
||||
if (r.hasHeader) {
|
||||
md << r.version << " | `" << r.catalogName << "` | "
|
||||
<< r.entryCount << " | ";
|
||||
} else {
|
||||
md << "- | - | - | ";
|
||||
}
|
||||
md << r.bytes << " |\n";
|
||||
}
|
||||
md << "\n";
|
||||
}
|
||||
md << "## How to inspect\n\n";
|
||||
md << "Use `--info-magic <path>` to identify any file by magic, "
|
||||
<< "or the per-format `--info-*` flag listed in `--list-formats`.\n";
|
||||
std::string output = md.str();
|
||||
if (outPath.empty()) {
|
||||
std::printf("%s", output.c_str());
|
||||
} else {
|
||||
std::ofstream out(outPath);
|
||||
if (!out) {
|
||||
std::fprintf(stderr,
|
||||
"tree-summary-md: cannot write %s\n",
|
||||
outPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
out << output;
|
||||
std::printf("Wrote %s\n", outPath.c_str());
|
||||
std::printf(" scanned files : %llu\n",
|
||||
static_cast<unsigned long long>(totalFiles));
|
||||
std::printf(" recognized .w* : %llu\n",
|
||||
static_cast<unsigned long long>(totalRecognized));
|
||||
std::printf(" unique formats : %zu\n",
|
||||
formatFileCounts.size());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleTreeSummaryMd(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--tree-summary-md") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleSummary(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
11
tools/editor/cli_tree_summary_md.hpp
Normal file
11
tools/editor/cli_tree_summary_md.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleTreeSummaryMd(int& i, int argc, char** argv, int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue