mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 11:03:51 +00:00
408 lines
17 KiB
C++
408 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
|