#include "cli_addon_manifest_catalog.hpp" #include "cli_arg_parse.hpp" #include "cli_box_emitter.hpp" #include "pipeline/wowee_addon_manifest.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace wowee { namespace editor { namespace cli { namespace { std::string stripWmodExt(std::string base) { stripExt(base, ".wmod"); return base; } bool saveOrError(const wowee::pipeline::WoweeAddonManifest& c, const std::string& base, const char* cmd) { if (!wowee::pipeline::WoweeAddonManifestLoader::save(c, base)) { std::fprintf(stderr, "%s: failed to save %s.wmod\n", cmd, base.c_str()); return false; } return true; } void printGenSummary(const wowee::pipeline::WoweeAddonManifest& c, const std::string& base) { std::printf("Wrote %s.wmod\n", base.c_str()); std::printf(" catalog : %s\n", c.name.c_str()); std::printf(" addons : %zu\n", c.entries.size()); } int handleGenStandard(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "StandardAddons"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWmodExt(base); auto c = wowee::pipeline::WoweeAddonManifestLoader:: makeStandardAddons(name); if (!saveOrError(c, base, "gen-mod")) return 1; printGenSummary(c, base); return 0; } int handleGenUI(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "UIReplacement"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWmodExt(base); auto c = wowee::pipeline::WoweeAddonManifestLoader:: makeUIReplacement(name); if (!saveOrError(c, base, "gen-mod-ui")) return 1; printGenSummary(c, base); return 0; } int handleGenUtility(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "UtilityAddons"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWmodExt(base); auto c = wowee::pipeline::WoweeAddonManifestLoader:: makeUtility(name); if (!saveOrError(c, base, "gen-mod-util")) return 1; printGenSummary(c, base); return 0; } int handleInfo(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); base = stripWmodExt(base); if (!wowee::pipeline::WoweeAddonManifestLoader::exists(base)) { std::fprintf(stderr, "WMOD not found: %s.wmod\n", base.c_str()); return 1; } auto c = wowee::pipeline::WoweeAddonManifestLoader::load(base); if (jsonOut) { nlohmann::json j; j["wmod"] = base + ".wmod"; j["name"] = c.name; j["count"] = c.entries.size(); nlohmann::json arr = nlohmann::json::array(); for (const auto& e : c.entries) { arr.push_back({ {"addonId", e.addonId}, {"name", e.name}, {"description", e.description}, {"version", e.version}, {"author", e.author}, {"minClientBuild", e.minClientBuild}, {"requiresSavedVariables", e.requiresSavedVariables != 0}, {"loadOnDemand", e.loadOnDemand != 0}, {"dependencies", e.dependencies}, {"optionalDependencies", e.optionalDependencies}, }); } j["entries"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WMOD: %s.wmod\n", base.c_str()); std::printf(" catalog : %s\n", c.name.c_str()); std::printf(" addons : %zu\n", c.entries.size()); if (c.entries.empty()) return 0; std::printf(" id version sv lod deps optDeps name\n"); for (const auto& e : c.entries) { std::printf(" %4u %-9s %s %s %4zu %7zu %s\n", e.addonId, e.version.c_str(), e.requiresSavedVariables ? "Y" : "n", e.loadOnDemand ? "Y" : "n", e.dependencies.size(), e.optionalDependencies.size(), e.name.c_str()); } return 0; } // Stack-based DFS cycle detection. Returns the first // cycle found as a vector of addonIds. Empty if no // cycle. Considers ONLY required dependencies — optional // deps don't deadlock. std::vector findFirstCycle( const wowee::pipeline::WoweeAddonManifest& c) { std::map> graph; std::set known; for (const auto& e : c.entries) { graph[e.addonId] = e.dependencies; known.insert(e.addonId); } enum Color : uint8_t { White = 0, Gray = 1, Black = 2 }; std::map color; for (uint32_t id : known) color[id] = White; std::vector path; std::vector cycle; std::function dfs = [&](uint32_t node) -> bool { color[node] = Gray; path.push_back(node); for (uint32_t dep : graph[node]) { if (!known.count(dep)) continue; if (color[dep] == Gray) { // Found back-edge to gray node — extract // the cycle starting at dep in path. auto it = std::find(path.begin(), path.end(), dep); cycle.assign(it, path.end()); cycle.push_back(dep); // close the loop return true; } if (color[dep] == White) { if (dfs(dep)) return true; } } color[node] = Black; path.pop_back(); return false; }; for (uint32_t id : known) { if (color[id] == White && dfs(id)) return cycle; } return {}; } int handleValidate(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); base = stripWmodExt(base); if (!wowee::pipeline::WoweeAddonManifestLoader::exists(base)) { std::fprintf(stderr, "validate-wmod: WMOD not found: %s.wmod\n", base.c_str()); return 1; } auto c = wowee::pipeline::WoweeAddonManifestLoader::load(base); std::vector errors; std::vector warnings; if (c.entries.empty()) { warnings.push_back("catalog has zero entries"); } std::set idsSeen; std::set namesSeen; std::set knownIds; for (const auto& e : c.entries) knownIds.insert(e.addonId); for (size_t k = 0; k < c.entries.size(); ++k) { const auto& e = c.entries[k]; std::string ctx = "entry " + std::to_string(k) + " (id=" + std::to_string(e.addonId); if (!e.name.empty()) ctx += " " + e.name; ctx += ")"; if (e.addonId == 0) errors.push_back(ctx + ": addonId is 0"); if (e.name.empty()) errors.push_back(ctx + ": name is empty"); if (e.version.empty()) errors.push_back(ctx + ": version is empty " "(every addon must declare a version)"); if (!e.name.empty() && !namesSeen.insert(e.name).second) { errors.push_back(ctx + ": duplicate addon name '" + e.name + "' — addon-loader would dispatch ambiguously"); } if (!idsSeen.insert(e.addonId).second) { errors.push_back(ctx + ": duplicate addonId"); } // Self-dependency: addon listing itself in its // own deps would deadlock during load. for (uint32_t dep : e.dependencies) { if (dep == e.addonId) { errors.push_back(ctx + ": addon depends on itself " "(deadlock at load)"); } if (!knownIds.count(dep)) { errors.push_back(ctx + ": required dependency addonId=" + std::to_string(dep) + " not found in catalog"); } } for (uint32_t dep : e.optionalDependencies) { if (dep == e.addonId) { warnings.push_back(ctx + ": addon optionally depends on " "itself — has no effect, prune"); } // Optional deps to unknown ids are NOT an // error — addon may degrade gracefully if // the optional dep is absent. } if (e.minClientBuild != 0 && e.minClientBuild < 4500) { warnings.push_back(ctx + ": minClientBuild=" + std::to_string(e.minClientBuild) + " is below the lowest known WoW vanilla " "build (4500); likely a typo"); } } // DFS cycle detection over required dependencies. auto cycle = findFirstCycle(c); if (!cycle.empty()) { std::string trail; for (size_t k = 0; k < cycle.size(); ++k) { if (k > 0) trail += " -> "; trail += std::to_string(cycle[k]); } errors.push_back("dependency cycle detected: " + trail + " — addon-loader would deadlock"); } bool ok = errors.empty(); if (jsonOut) { nlohmann::json j; j["wmod"] = base + ".wmod"; j["ok"] = ok; j["errors"] = errors; j["warnings"] = warnings; std::printf("%s\n", j.dump(2).c_str()); return ok ? 0 : 1; } std::printf("validate-wmod: %s.wmod\n", base.c_str()); if (ok && warnings.empty()) { std::printf(" OK — %zu addons, all addonIds + " "names unique, no required-dep cycle, " "no missing required deps, no self-" "deps\n", c.entries.size()); return 0; } if (!warnings.empty()) { std::printf(" warnings (%zu):\n", warnings.size()); for (const auto& w : warnings) std::printf(" - %s\n", w.c_str()); } if (!errors.empty()) { std::printf(" ERRORS (%zu):\n", errors.size()); for (const auto& e : errors) std::printf(" - %s\n", e.c_str()); } return ok ? 0 : 1; } } // namespace bool handleAddonManifestCatalog(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-mod") == 0 && i + 1 < argc) { outRc = handleGenStandard(i, argc, argv); return true; } if (std::strcmp(argv[i], "--gen-mod-ui") == 0 && i + 1 < argc) { outRc = handleGenUI(i, argc, argv); return true; } if (std::strcmp(argv[i], "--gen-mod-util") == 0 && i + 1 < argc) { outRc = handleGenUtility(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-wmod") == 0 && i + 1 < argc) { outRc = handleInfo(i, argc, argv); return true; } if (std::strcmp(argv[i], "--validate-wmod") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee