mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-09 18:43:51 +00:00
Moves the three asset-dependency analysis handlers
(--list-zone-deps, --list-project-orphans,
--remove-project-orphans) out of main.cpp into a new
cli_deps.{hpp,cpp} module. All three surface the relationship
between content references (objects.json + WOB doodad lists)
and on-disk model files: list-deps enumerates what a zone
needs, list-orphans flips that into 'what's on disk but
unreferenced', and remove-orphans deletes the resulting set
(with --dry-run for safe previews).
main.cpp shrinks by 355 lines (3,319 to 2,964) and finally
drops below 3K. The shared 'normalize basename' rule for
matching path references stays duplicated between the list
and remove handlers — a deliberate tradeoff, with comments
flagging the sync requirement.
407 lines
17 KiB
C++
407 lines
17 KiB
C++
#include "cli_deps.hpp"
|
||
|
||
#include "object_placer.hpp"
|
||
#include "pipeline/wowee_building.hpp"
|
||
#include <nlohmann/json.hpp>
|
||
|
||
#include <algorithm>
|
||
#include <cstdint>
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <filesystem>
|
||
#include <map>
|
||
#include <set>
|
||
#include <string>
|
||
#include <system_error>
|
||
#include <vector>
|
||
|
||
namespace wowee {
|
||
namespace editor {
|
||
namespace cli {
|
||
|
||
namespace {
|
||
|
||
int handleListZoneDeps(int& i, int argc, char** argv) {
|
||
// Enumerate every external model path a zone references —
|
||
// both directly placed (objects.json) and indirectly via
|
||
// doodad placements inside any WOB sitting next to the
|
||
// zone manifest. Useful when packaging a content pack to
|
||
// confirm every needed asset will ship.
|
||
std::string zoneDir = argv[++i];
|
||
bool jsonOut = (i + 1 < argc &&
|
||
std::strcmp(argv[i + 1], "--json") == 0);
|
||
if (jsonOut) i++;
|
||
namespace fs = std::filesystem;
|
||
if (!fs::exists(zoneDir + "/zone.json")) {
|
||
std::fprintf(stderr,
|
||
"list-zone-deps: %s has no zone.json\n", zoneDir.c_str());
|
||
return 1;
|
||
}
|
||
// Collect with usage counts so duplicates report '×N' instead
|
||
// of cluttering the table.
|
||
std::map<std::string, int> directM2; // m2 placements
|
||
std::map<std::string, int> directWMO; // wmo placements
|
||
std::map<std::string, int> doodadM2; // m2s referenced inside WOBs
|
||
wowee::editor::ObjectPlacer op;
|
||
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
||
for (const auto& o : op.getObjects()) {
|
||
if (o.type == wowee::editor::PlaceableType::M2) directM2[o.path]++;
|
||
else if (o.type == wowee::editor::PlaceableType::WMO) directWMO[o.path]++;
|
||
}
|
||
}
|
||
// Walk WOBs in the zone directory recursively and pull in
|
||
// their doodad model paths. Sub-dirs caught too in case the
|
||
// user organizes buildings under a buildings/ subfolder.
|
||
int wobCount = 0;
|
||
std::error_code ec;
|
||
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
std::string ext = e.path().extension().string();
|
||
if (ext != ".wob") continue;
|
||
wobCount++;
|
||
std::string base = e.path().string();
|
||
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
||
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
||
for (const auto& d : bld.doodads) {
|
||
if (d.modelPath.empty()) continue;
|
||
doodadM2[d.modelPath]++;
|
||
}
|
||
}
|
||
// For each direct WMO placement, also recurse into the WOB
|
||
// sitting at that path (relative to the zone) so transitive
|
||
// doodad deps surface — this matches the runtime's actual
|
||
// load chain.
|
||
for (const auto& [path, count] : directWMO) {
|
||
// Strip extension since loader takes a base path.
|
||
std::string base = path;
|
||
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wmo")
|
||
base = base.substr(0, base.size() - 4);
|
||
// Try relative-to-zone first, then absolute.
|
||
std::string trial = zoneDir + "/" + base;
|
||
if (!wowee::pipeline::WoweeBuildingLoader::exists(trial)) trial = base;
|
||
if (!wowee::pipeline::WoweeBuildingLoader::exists(trial)) continue;
|
||
auto bld = wowee::pipeline::WoweeBuildingLoader::load(trial);
|
||
for (const auto& d : bld.doodads) {
|
||
if (d.modelPath.empty()) continue;
|
||
doodadM2[d.modelPath]++;
|
||
}
|
||
}
|
||
size_t totalUnique = directM2.size() + directWMO.size() + doodadM2.size();
|
||
if (jsonOut) {
|
||
nlohmann::json j;
|
||
j["zone"] = zoneDir;
|
||
j["wobCount"] = wobCount;
|
||
j["totalUnique"] = totalUnique;
|
||
auto toArr = [](const std::map<std::string, int>& m) {
|
||
nlohmann::json a = nlohmann::json::array();
|
||
for (const auto& [path, count] : m) {
|
||
a.push_back({{"path", path}, {"count", count}});
|
||
}
|
||
return a;
|
||
};
|
||
j["directM2"] = toArr(directM2);
|
||
j["directWMO"] = toArr(directWMO);
|
||
j["doodadM2"] = toArr(doodadM2);
|
||
std::printf("%s\n", j.dump(2).c_str());
|
||
return 0;
|
||
}
|
||
std::printf("Zone deps: %s\n", zoneDir.c_str());
|
||
std::printf(" WOBs scanned : %d\n", wobCount);
|
||
std::printf(" unique paths total : %zu\n", totalUnique);
|
||
auto emit = [](const char* tag, const std::map<std::string, int>& m) {
|
||
std::printf("\n %s (%zu unique):\n", tag, m.size());
|
||
if (m.empty()) {
|
||
std::printf(" *none*\n");
|
||
return;
|
||
}
|
||
for (const auto& [path, count] : m) {
|
||
if (count > 1) std::printf(" %s ×%d\n", path.c_str(), count);
|
||
else std::printf(" %s\n", path.c_str());
|
||
}
|
||
};
|
||
emit("Direct M2 placements", directM2);
|
||
emit("Direct WMO placements", directWMO);
|
||
emit("WOB doodad M2 refs", doodadM2);
|
||
return 0;
|
||
}
|
||
|
||
int handleListProjectOrphans(int& i, int argc, char** argv) {
|
||
// Inverse of --list-zone-deps. Walks every zone in
|
||
// <projectDir>, collects the set of .wom/.wob files
|
||
// sitting on disk and the set of paths actually
|
||
// referenced by objects.json placements + WOB doodad
|
||
// lists. Files in the first set but not the second are
|
||
// orphans — candidates for removal before --pack-wcp so
|
||
// the archive doesn't carry dead weight.
|
||
//
|
||
// Comparison is by basename (extension stripped) since
|
||
// the reference paths sometimes include the extension and
|
||
// sometimes don't.
|
||
std::string projectDir = argv[++i];
|
||
bool jsonOut = (i + 1 < argc &&
|
||
std::strcmp(argv[i + 1], "--json") == 0);
|
||
if (jsonOut) i++;
|
||
namespace fs = std::filesystem;
|
||
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
||
std::fprintf(stderr,
|
||
"list-project-orphans: %s is not a directory\n",
|
||
projectDir.c_str());
|
||
return 1;
|
||
}
|
||
std::vector<std::string> zones;
|
||
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
||
if (!entry.is_directory()) continue;
|
||
if (!fs::exists(entry.path() / "zone.json")) continue;
|
||
zones.push_back(entry.path().string());
|
||
}
|
||
std::sort(zones.begin(), zones.end());
|
||
// Project-wide reference set. Normalize by stripping
|
||
// extension and any leading "./".
|
||
auto normalize = [](std::string p) {
|
||
while (p.size() >= 2 && p[0] == '.' && p[1] == '/') p.erase(0, 2);
|
||
std::string ext = fs::path(p).extension().string();
|
||
if (ext == ".wom" || ext == ".wob" || ext == ".m2" || ext == ".wmo") {
|
||
p = p.substr(0, p.size() - ext.size());
|
||
}
|
||
return p;
|
||
};
|
||
std::set<std::string> referencedBases; // normalized basenames
|
||
for (const auto& zoneDir : zones) {
|
||
wowee::editor::ObjectPlacer op;
|
||
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
||
for (const auto& o : op.getObjects()) {
|
||
if (o.path.empty()) continue;
|
||
// Reference can be relative to zone or just a
|
||
// bare model name; record both forms for the
|
||
// membership test.
|
||
std::string norm = normalize(o.path);
|
||
referencedBases.insert(norm);
|
||
// Also try the leaf basename so unqualified
|
||
// refs match.
|
||
referencedBases.insert(fs::path(norm).filename().string());
|
||
}
|
||
}
|
||
std::error_code ec;
|
||
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
if (e.path().extension() != ".wob") continue;
|
||
std::string base = e.path().string();
|
||
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
||
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
||
for (const auto& d : bld.doodads) {
|
||
if (d.modelPath.empty()) continue;
|
||
std::string norm = normalize(d.modelPath);
|
||
referencedBases.insert(norm);
|
||
referencedBases.insert(fs::path(norm).filename().string());
|
||
}
|
||
}
|
||
}
|
||
// Now walk every zone again and flag orphan .wom/.wob files.
|
||
struct Orphan { std::string zone, path; uint64_t bytes; };
|
||
std::vector<Orphan> orphans;
|
||
uint64_t totalOrphanBytes = 0;
|
||
for (const auto& zoneDir : zones) {
|
||
std::string zoneName = fs::path(zoneDir).filename().string();
|
||
std::error_code ec;
|
||
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
std::string ext = e.path().extension().string();
|
||
if (ext != ".wom" && ext != ".wob") continue;
|
||
std::string rel = fs::relative(e.path(), zoneDir, ec).string();
|
||
if (ec) rel = e.path().filename().string();
|
||
std::string normRel = rel.substr(0, rel.size() - ext.size());
|
||
std::string leaf = e.path().stem().string();
|
||
if (referencedBases.count(normRel) ||
|
||
referencedBases.count(leaf)) {
|
||
continue; // referenced, not orphan
|
||
}
|
||
uint64_t sz = e.file_size(ec);
|
||
if (ec) sz = 0;
|
||
orphans.push_back({zoneName, rel, sz});
|
||
totalOrphanBytes += sz;
|
||
}
|
||
}
|
||
std::sort(orphans.begin(), orphans.end(),
|
||
[](const Orphan& a, const Orphan& b) {
|
||
if (a.zone != b.zone) return a.zone < b.zone;
|
||
return a.path < b.path;
|
||
});
|
||
if (jsonOut) {
|
||
nlohmann::json j;
|
||
j["project"] = projectDir;
|
||
j["referencedCount"] = referencedBases.size();
|
||
j["orphanCount"] = orphans.size();
|
||
j["orphanBytes"] = totalOrphanBytes;
|
||
nlohmann::json arr = nlohmann::json::array();
|
||
for (const auto& o : orphans) {
|
||
arr.push_back({{"zone", o.zone},
|
||
{"path", o.path},
|
||
{"bytes", o.bytes}});
|
||
}
|
||
j["orphans"] = arr;
|
||
std::printf("%s\n", j.dump(2).c_str());
|
||
return 0;
|
||
}
|
||
std::printf("Project orphans: %s\n", projectDir.c_str());
|
||
std::printf(" zones scanned : %zu\n", zones.size());
|
||
std::printf(" refs collected : %zu (normalized basenames)\n",
|
||
referencedBases.size());
|
||
std::printf(" orphan .wom/.wob : %zu file(s), %.1f KB\n",
|
||
orphans.size(), totalOrphanBytes / 1024.0);
|
||
if (orphans.empty()) {
|
||
std::printf("\n (no orphans — every model file is referenced)\n");
|
||
return 0;
|
||
}
|
||
std::printf("\n zone bytes path\n");
|
||
for (const auto& o : orphans) {
|
||
std::printf(" %-20s %8llu %s\n",
|
||
o.zone.substr(0, 20).c_str(),
|
||
static_cast<unsigned long long>(o.bytes),
|
||
o.path.c_str());
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int handleRemoveProjectOrphans(int& i, int argc, char** argv) {
|
||
// Destructive companion to --list-project-orphans. Reuses
|
||
// the same reference-collection + orphan-detection logic
|
||
// and then deletes the resulting files. --dry-run shows
|
||
// what would be removed without touching anything.
|
||
std::string projectDir = argv[++i];
|
||
bool dryRun = false;
|
||
if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) {
|
||
dryRun = true; i++;
|
||
}
|
||
namespace fs = std::filesystem;
|
||
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
|
||
std::fprintf(stderr,
|
||
"remove-project-orphans: %s is not a directory\n",
|
||
projectDir.c_str());
|
||
return 1;
|
||
}
|
||
std::vector<std::string> zones;
|
||
for (const auto& entry : fs::directory_iterator(projectDir)) {
|
||
if (!entry.is_directory()) continue;
|
||
if (!fs::exists(entry.path() / "zone.json")) continue;
|
||
zones.push_back(entry.path().string());
|
||
}
|
||
std::sort(zones.begin(), zones.end());
|
||
// Same normalize + reference collection as --list-project-orphans.
|
||
// Keep both functions in sync if the matching rules evolve.
|
||
auto normalize = [](std::string p) {
|
||
while (p.size() >= 2 && p[0] == '.' && p[1] == '/') p.erase(0, 2);
|
||
std::string ext = fs::path(p).extension().string();
|
||
if (ext == ".wom" || ext == ".wob" || ext == ".m2" || ext == ".wmo") {
|
||
p = p.substr(0, p.size() - ext.size());
|
||
}
|
||
return p;
|
||
};
|
||
std::set<std::string> referencedBases;
|
||
for (const auto& zoneDir : zones) {
|
||
wowee::editor::ObjectPlacer op;
|
||
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
||
for (const auto& o : op.getObjects()) {
|
||
if (o.path.empty()) continue;
|
||
std::string norm = normalize(o.path);
|
||
referencedBases.insert(norm);
|
||
referencedBases.insert(fs::path(norm).filename().string());
|
||
}
|
||
}
|
||
std::error_code ec;
|
||
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
if (e.path().extension() != ".wob") continue;
|
||
std::string base = e.path().string();
|
||
if (base.size() >= 4) base = base.substr(0, base.size() - 4);
|
||
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
||
for (const auto& d : bld.doodads) {
|
||
if (d.modelPath.empty()) continue;
|
||
std::string norm = normalize(d.modelPath);
|
||
referencedBases.insert(norm);
|
||
referencedBases.insert(fs::path(norm).filename().string());
|
||
}
|
||
}
|
||
}
|
||
int removed = 0, failed = 0;
|
||
uint64_t freedBytes = 0;
|
||
for (const auto& zoneDir : zones) {
|
||
std::string zoneName = fs::path(zoneDir).filename().string();
|
||
std::error_code ec;
|
||
std::vector<fs::path> toRemove;
|
||
for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) {
|
||
if (!e.is_regular_file()) continue;
|
||
std::string ext = e.path().extension().string();
|
||
if (ext != ".wom" && ext != ".wob") continue;
|
||
std::string rel = fs::relative(e.path(), zoneDir, ec).string();
|
||
if (ec) rel = e.path().filename().string();
|
||
std::string normRel = rel.substr(0, rel.size() - ext.size());
|
||
std::string leaf = e.path().stem().string();
|
||
if (referencedBases.count(normRel) ||
|
||
referencedBases.count(leaf)) continue;
|
||
toRemove.push_back(e.path());
|
||
}
|
||
// Materialize the deletion list before removing so we
|
||
// don't mutate the directory while iterating.
|
||
for (const auto& p : toRemove) {
|
||
uint64_t sz = fs::file_size(p, ec);
|
||
if (ec) sz = 0;
|
||
std::string rel = fs::relative(p, zoneDir, ec).string();
|
||
if (ec) rel = p.filename().string();
|
||
if (dryRun) {
|
||
std::printf(" would remove: %s/%s (%llu bytes)\n",
|
||
zoneName.c_str(), rel.c_str(),
|
||
static_cast<unsigned long long>(sz));
|
||
removed++;
|
||
freedBytes += sz;
|
||
} else {
|
||
if (fs::remove(p, ec)) {
|
||
std::printf(" removed: %s/%s (%llu bytes)\n",
|
||
zoneName.c_str(), rel.c_str(),
|
||
static_cast<unsigned long long>(sz));
|
||
removed++;
|
||
freedBytes += sz;
|
||
} else {
|
||
std::fprintf(stderr,
|
||
" WARN: failed to remove %s (%s)\n",
|
||
p.c_str(), ec.message().c_str());
|
||
failed++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
std::printf("\nremove-project-orphans: %s%s\n",
|
||
projectDir.c_str(), dryRun ? " (dry-run)" : "");
|
||
std::printf(" zones : %zu\n", zones.size());
|
||
std::printf(" refs : %zu (normalized basenames)\n",
|
||
referencedBases.size());
|
||
std::printf(" %s : %d file(s)\n",
|
||
dryRun ? "would remove" : "removed ", removed);
|
||
std::printf(" freed : %.1f KB\n", freedBytes / 1024.0);
|
||
if (failed > 0) {
|
||
std::printf(" FAILED : %d (see stderr)\n", failed);
|
||
}
|
||
if (dryRun && removed > 0) {
|
||
std::printf(" re-run without --dry-run to apply\n");
|
||
}
|
||
return failed == 0 ? 0 : 1;
|
||
}
|
||
|
||
|
||
} // namespace
|
||
|
||
bool handleDeps(int& i, int argc, char** argv, int& outRc) {
|
||
if (std::strcmp(argv[i], "--list-zone-deps") == 0 && i + 1 < argc) {
|
||
outRc = handleListZoneDeps(i, argc, argv); return true;
|
||
}
|
||
if (std::strcmp(argv[i], "--list-project-orphans") == 0 && i + 1 < argc) {
|
||
outRc = handleListProjectOrphans(i, argc, argv); return true;
|
||
}
|
||
if (std::strcmp(argv[i], "--remove-project-orphans") == 0 && i + 1 < argc) {
|
||
outRc = handleRemoveProjectOrphans(i, argc, argv); return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
} // namespace cli
|
||
} // namespace editor
|
||
} // namespace wowee
|