#include "cli_mail_catalog.hpp" #include "cli_arg_parse.hpp" #include "cli_box_emitter.hpp" #include "pipeline/wowee_mail.hpp" #include #include #include #include #include #include #include namespace wowee { namespace editor { namespace cli { namespace { std::string stripWmalExt(std::string base) { stripExt(base, ".wmal"); return base; } bool saveOrError(const wowee::pipeline::WoweeMail& c, const std::string& base, const char* cmd) { if (!wowee::pipeline::WoweeMailLoader::save(c, base)) { std::fprintf(stderr, "%s: failed to save %s.wmal\n", cmd, base.c_str()); return false; } return true; } void printGenSummary(const wowee::pipeline::WoweeMail& c, const std::string& base) { std::printf("Wrote %s.wmal\n", base.c_str()); std::printf(" catalog : %s\n", c.name.c_str()); std::printf(" templates : %zu\n", c.entries.size()); } int handleGenStarter(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "StarterMail"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWmalExt(base); auto c = wowee::pipeline::WoweeMailLoader::makeStarter(name); if (!saveOrError(c, base, "gen-mail")) return 1; printGenSummary(c, base); return 0; } int handleGenHoliday(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "HolidayMail"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWmalExt(base); auto c = wowee::pipeline::WoweeMailLoader::makeHoliday(name); if (!saveOrError(c, base, "gen-mail-holiday")) return 1; printGenSummary(c, base); return 0; } int handleGenAuction(int& i, int argc, char** argv) { std::string base = argv[++i]; std::string name = "AuctionMail"; if (parseOptArg(i, argc, argv)) name = argv[++i]; base = stripWmalExt(base); auto c = wowee::pipeline::WoweeMailLoader::makeAuction(name); if (!saveOrError(c, base, "gen-mail-auction")) 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 = stripWmalExt(base); if (!wowee::pipeline::WoweeMailLoader::exists(base)) { std::fprintf(stderr, "WMAL not found: %s.wmal\n", base.c_str()); return 1; } auto c = wowee::pipeline::WoweeMailLoader::load(base); if (jsonOut) { nlohmann::json j; j["wmal"] = base + ".wmal"; j["name"] = c.name; j["count"] = c.entries.size(); nlohmann::json arr = nlohmann::json::array(); for (const auto& e : c.entries) { nlohmann::json je; je["templateId"] = e.templateId; je["senderNpcId"] = e.senderNpcId; je["subject"] = e.subject; je["body"] = e.body; je["senderName"] = e.senderName; je["moneyCopperAttached"] = e.moneyCopperAttached; je["categoryId"] = e.categoryId; je["categoryName"] = wowee::pipeline::WoweeMail::categoryName(e.categoryId); je["cod"] = e.cod; je["returnable"] = e.returnable; je["expiryDays"] = e.expiryDays; nlohmann::json att = nlohmann::json::array(); for (const auto& a : e.attachments) { att.push_back({{"itemId", a.itemId}, {"quantity", a.quantity}}); } je["attachments"] = att; arr.push_back(je); } j["entries"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WMAL: %s.wmal\n", base.c_str()); std::printf(" catalog : %s\n", c.name.c_str()); std::printf(" templates : %zu\n", c.entries.size()); if (c.entries.empty()) return 0; for (const auto& e : c.entries) { std::printf("\n templateId=%u category=%s expires=%ud cod=%u return=%u\n", e.templateId, wowee::pipeline::WoweeMail::categoryName(e.categoryId), e.expiryDays, e.cod, e.returnable); if (e.senderNpcId) { std::printf(" sender : %s (npcId=%u)\n", e.senderName.c_str(), e.senderNpcId); } else { std::printf(" sender : %s\n", e.senderName.c_str()); } std::printf(" subject : %s\n", e.subject.c_str()); if (!e.body.empty()) { std::printf(" body : %s\n", e.body.c_str()); } if (e.moneyCopperAttached) { std::printf(" money : %uc\n", e.moneyCopperAttached); } if (!e.attachments.empty()) { std::printf(" items :"); for (const auto& a : e.attachments) { std::printf(" item%u x%u", a.itemId, a.quantity); } std::printf("\n"); } } return 0; } int handleValidate(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); base = stripWmalExt(base); if (!wowee::pipeline::WoweeMailLoader::exists(base)) { std::fprintf(stderr, "validate-wmal: WMAL not found: %s.wmal\n", base.c_str()); return 1; } auto c = wowee::pipeline::WoweeMailLoader::load(base); std::vector errors; std::vector warnings; if (c.entries.empty()) { warnings.push_back("catalog has zero entries"); } std::vector 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.templateId) + ")"; if (e.templateId == 0) errors.push_back(ctx + ": templateId is 0"); if (e.subject.empty()) errors.push_back(ctx + ": subject is empty"); if (e.senderNpcId == 0 && e.senderName.empty()) { errors.push_back(ctx + ": neither senderNpcId nor senderName set (no displayable sender)"); } if (e.categoryId > wowee::pipeline::WoweeMail::ReturnedMail) { errors.push_back(ctx + ": categoryId " + std::to_string(e.categoryId) + " not in 0..7"); } if (e.expiryDays == 0) { warnings.push_back(ctx + ": expiryDays=0 (mail expires immediately)"); } if (e.cod && e.moneyCopperAttached == 0) { warnings.push_back(ctx + ": cod=1 but moneyCopperAttached=0 (free COD)"); } // Mail with no money + no items is informational only. // Legitimate for GM correspondence (text-only notices) // and for the Auction category where the runtime fills // in the real outcome (winning bid amount / sold item) // at send time. Flag only for the categories where // empty mail is genuinely a typo. if (e.moneyCopperAttached == 0 && e.attachments.empty() && e.categoryId != wowee::pipeline::WoweeMail::GmCorrespondence && e.categoryId != wowee::pipeline::WoweeMail::Auction && e.categoryId != wowee::pipeline::WoweeMail::ReturnedMail) { warnings.push_back(ctx + ": no money + no items (informational mail only)"); } for (size_t ai = 0; ai < e.attachments.size(); ++ai) { const auto& a = e.attachments[ai]; if (a.itemId == 0) { errors.push_back(ctx + " attachment " + std::to_string(ai) + ": itemId is 0"); } if (a.quantity == 0) { errors.push_back(ctx + " attachment " + std::to_string(ai) + ": quantity is 0"); } } for (uint32_t prev : idsSeen) { if (prev == e.templateId) { errors.push_back(ctx + ": duplicate templateId"); break; } } idsSeen.push_back(e.templateId); } bool ok = errors.empty(); if (jsonOut) { nlohmann::json j; j["wmal"] = base + ".wmal"; 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-wmal: %s.wmal\n", base.c_str()); if (ok && warnings.empty()) { std::printf(" OK — %zu templates, all templateIds unique\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 handleMailCatalog(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-mail") == 0 && i + 1 < argc) { outRc = handleGenStarter(i, argc, argv); return true; } if (std::strcmp(argv[i], "--gen-mail-holiday") == 0 && i + 1 < argc) { outRc = handleGenHoliday(i, argc, argv); return true; } if (std::strcmp(argv[i], "--gen-mail-auction") == 0 && i + 1 < argc) { outRc = handleGenAuction(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-wmal") == 0 && i + 1 < argc) { outRc = handleInfo(i, argc, argv); return true; } if (std::strcmp(argv[i], "--validate-wmal") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee