#include "cli_chat_links_catalog.hpp" #include "cli_arg_parse.hpp" #include "cli_box_emitter.hpp" #include "pipeline/wowee_chat_links.hpp" #include #include #include #include #include #include #include #include namespace wowee { namespace editor { namespace cli { namespace { std::string stripWlnkExt(std::string base) { stripExt(base, ".wlnk"); return base; } const char* linkKindName(uint8_t k) { using L = wowee::pipeline::WoweeChatLinks; switch (k) { case L::Item: return "item"; case L::Quest: return "quest"; case L::Spell: return "spell"; case L::Achievement: return "achievement"; case L::Talent: return "talent"; case L::Trade: return "trade"; default: return "?"; } } bool saveOrError(const wowee::pipeline::WoweeChatLinks& c, const std::string& base, const char* cmd) { if (!wowee::pipeline::WoweeChatLinksLoader::save(c, base)) { std::fprintf(stderr, "%s: failed to save %s.wlnk\n", cmd, base.c_str()); return false; } return true; } void printGenSummary(const wowee::pipeline::WoweeChatLinks& c, const std::string& base) { std::printf("Wrote %s.wlnk\n", base.c_str()); std::printf(" catalog : %s\n", c.name.c_str()); std::printf(" links : %zu\n", c.entries.size()); } int handleGenStandard(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "StandardChatLinks"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWlnkExt(base); auto c = wowee::pipeline::WoweeChatLinksLoader:: makeStandardLinks(name); if (!saveOrError(c, base, "gen-lnk-std")) return 1; printGenSummary(c, base); return 0; } int handleGenTalentTrade(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "TalentTradeChatLinks"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWlnkExt(base); auto c = wowee::pipeline::WoweeChatLinksLoader:: makeTalentTrade(name); if (!saveOrError(c, base, "gen-lnk-talent")) return 1; printGenSummary(c, base); return 0; } int handleGenColorVariants(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "ItemQualityColorVariants"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWlnkExt(base); auto c = wowee::pipeline::WoweeChatLinksLoader:: makeColorVariants(name); if (!saveOrError(c, base, "gen-lnk-quality")) 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 = stripWlnkExt(base); if (!wowee::pipeline::WoweeChatLinksLoader::exists(base)) { std::fprintf(stderr, "WLNK not found: %s.wlnk\n", base.c_str()); return 1; } auto c = wowee::pipeline::WoweeChatLinksLoader::load(base); if (jsonOut) { nlohmann::json j; j["wlnk"] = base + ".wlnk"; j["name"] = c.name; j["count"] = c.entries.size(); nlohmann::json arr = nlohmann::json::array(); for (const auto& e : c.entries) { arr.push_back({ {"linkId", e.linkId}, {"name", e.name}, {"linkKind", e.linkKind}, {"linkKindName", linkKindName(e.linkKind)}, {"requireServerLookup", e.requireServerLookup != 0}, {"colorRGBA", e.colorRGBA}, {"linkTemplate", e.linkTemplate}, {"tooltipTemplate", e.tooltipTemplate}, {"iconRule", e.iconRule}, }); } j["entries"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WLNK: %s.wlnk\n", base.c_str()); std::printf(" catalog : %s\n", c.name.c_str()); std::printf(" links : %zu\n", c.entries.size()); if (c.entries.empty()) return 0; std::printf(" id kind srv color icon name\n"); for (const auto& e : c.entries) { std::printf(" %4u %-11s %s 0x%08X %-11s %s\n", e.linkId, linkKindName(e.linkKind), e.requireServerLookup ? "Y" : "n", e.colorRGBA, e.iconRule.c_str(), e.name.c_str()); } return 0; } // Counts %d / %s placeholder occurrences in a sprintf // template. Used by the validator to catch templates // with no placeholders (would never substitute the // link parameters). int countPlaceholders(const std::string& tpl) { int count = 0; for (size_t i = 0; i + 1 < tpl.size(); ++i) { if (tpl[i] != '%') continue; char c = tpl[i + 1]; if (c == 'd' || c == 's' || c == 'u' || c == 'i' || c == 'x' || c == 'X') { ++count; ++i; // skip the format char } else if (c == '%') { ++i; // literal %% — don't count } } return count; } int handleValidate(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); base = stripWlnkExt(base); if (!wowee::pipeline::WoweeChatLinksLoader::exists(base)) { std::fprintf(stderr, "validate-wlnk: WLNK not found: %s.wlnk\n", base.c_str()); return 1; } auto c = wowee::pipeline::WoweeChatLinksLoader::load(base); std::vector errors; std::vector warnings; if (c.entries.empty()) { warnings.push_back("catalog has zero entries"); } std::set idsSeen; 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.linkId); if (!e.name.empty()) ctx += " " + e.name; ctx += ")"; if (e.linkId == 0) errors.push_back(ctx + ": linkId is 0"); if (e.name.empty()) errors.push_back(ctx + ": name is empty"); if (e.linkTemplate.empty()) { errors.push_back(ctx + ": linkTemplate is empty — link " "composer would have nothing to " "format"); } if (e.linkKind > 5) { errors.push_back(ctx + ": linkKind " + std::to_string(e.linkKind) + " out of range (0..5)"); } // CRITICAL: linkTemplate MUST contain at // least one %d or %s placeholder. A template // with no placeholders never substitutes link // parameters — the chat composer would emit // a static string regardless of which item / // quest / spell was clicked. int placeholderCount = countPlaceholders(e.linkTemplate); if (!e.linkTemplate.empty() && placeholderCount == 0) { errors.push_back(ctx + ": linkTemplate has no %%d / %%s " "placeholders — composer would emit " "a static string regardless of input " "(every link would render identically)"); } // Warn on excessive placeholders (> 12) — // the achievement template legitimately has // 9 (achievementId + chardate + 5 progress // criteria + completion state + name) but // anything beyond ~12 is suspicious. if (placeholderCount > 12) { warnings.push_back(ctx + ": linkTemplate has " + std::to_string(placeholderCount) + " placeholders — > 12 is unusual; " "verify all are intentional"); } // colorRGBA = 0 means fully transparent — // link text would be invisible. Warn. if (e.colorRGBA == 0) { warnings.push_back(ctx + ": colorRGBA is 0 (fully transparent)" " — link text would be invisible; " "set a quality color"); } // requireServerLookup=true with no template // %s for receiving server-side data is // suspicious but not definitive (server may // populate the tooltip rather than the link // text). Warn. if (e.requireServerLookup && e.tooltipTemplate.empty()) { warnings.push_back(ctx + ": requireServerLookup=true but " "tooltipTemplate is empty — server " "data would not be displayed anywhere " "(verify intentional)"); } if (!idsSeen.insert(e.linkId).second) { errors.push_back(ctx + ": duplicate linkId"); } } bool ok = errors.empty(); if (jsonOut) { nlohmann::json j; j["wlnk"] = base + ".wlnk"; 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-wlnk: %s.wlnk\n", base.c_str()); if (ok && warnings.empty()) { std::printf(" OK — %zu links, all linkIds unique, " "linkKind 0..5, linkTemplate non-empty " "with at least one %%d/%%s placeholder, " "non-zero colorRGBA\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 handleChatLinksCatalog(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-lnk-std") == 0 && i + 1 < argc) { outRc = handleGenStandard(i, argc, argv); return true; } if (std::strcmp(argv[i], "--gen-lnk-talent") == 0 && i + 1 < argc) { outRc = handleGenTalentTrade(i, argc, argv); return true; } if (std::strcmp(argv[i], "--gen-lnk-quality") == 0 && i + 1 < argc) { outRc = handleGenColorVariants(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-wlnk") == 0 && i + 1 < argc) { outRc = handleInfo(i, argc, argv); return true; } if (std::strcmp(argv[i], "--validate-wlnk") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee