diff --git a/CMakeLists.txt b/CMakeLists.txt index dd69f30c..47c0abcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -675,6 +675,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_item_qualities.cpp src/pipeline/wowee_skill_costs.cpp src/pipeline/wowee_item_flags.cpp + src/pipeline/wowee_npc_services.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1511,6 +1512,7 @@ add_executable(wowee_editor tools/editor/cli_item_qualities_catalog.cpp tools/editor/cli_skill_costs_catalog.cpp tools/editor/cli_item_flags_catalog.cpp + tools/editor/cli_npc_services_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1664,6 +1666,7 @@ add_executable(wowee_editor src/pipeline/wowee_item_qualities.cpp src/pipeline/wowee_skill_costs.cpp src/pipeline/wowee_item_flags.cpp + src/pipeline/wowee_npc_services.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_npc_services.hpp b/include/pipeline/wowee_npc_services.hpp new file mode 100644 index 00000000..c80f3322 --- /dev/null +++ b/include/pipeline/wowee_npc_services.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open NPC Service Definition catalog (.wbkd) — +// novel replacement for AzerothCore's npc_vendor / +// npc_trainer / npc_gossip / npc_options SQL tables plus +// the engine's hard-coded service-type dispatch. Defines +// the kinds of services NPCs can offer (Banker / Mailbox +// / Auctioneer / StableMaster / FlightMaster / Trainer / +// Innkeeper / Battlemaster / etc) and the per-service +// metadata (gold cost, faction gating, gossip text). +// +// When a player right-clicks an NPC, the engine looks +// at the NPC's serviceId list (from WCRT.npcFlags or +// equivalent) and dispatches to the appropriate +// service-frame handler — Banker opens the inventory +// expansion frame, Auctioneer opens the auction house, +// StableMaster opens the pet stable. This catalog +// defines what each service actually does and what +// preconditions it requires. +// +// Cross-references with previously-added formats: +// WCRT: creature.npcFlags decodes into a list of +// service ids defined here. +// WFAC: factionRequiredId references WFAC.factionId +// for rep-gated services (Argent Tournament +// vendor only sells to Honored+). +// WGSP: gossipTextId references WGSP.menuId for the +// "How can I help you?" dialogue line. +// +// Binary layout (little-endian): +// magic[4] = "WBKD" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// serviceId (uint32) +// nameLen + name +// descLen + description +// serviceKind (uint8) / pad[3] +// requiresGold (uint32) +// factionRequiredId (uint32) +// gossipTextId (uint32) +// iconColorRGBA (uint32) +struct WoweeNPCService { + enum ServiceKind : uint8_t { + Banker = 0, // opens bank inventory frame + Mailbox = 1, // mail send/receive frame + Auctioneer = 2, // auction house frame + StableMaster = 3, // hunter pet stable frame + FlightMaster = 4, // taxi node selection frame + Trainer = 5, // class/profession trainer frame + Innkeeper = 6, // hearthstone bind point + bed + Battlemaster = 7, // BG queue frame + GuildBanker = 8, // guild bank frame (TBC+) + ReagentVendor = 9, // reagent purchase + TabardVendor = 10, // guild tabard customization + Misc = 11, // catch-all + }; + + struct Entry { + uint32_t serviceId = 0; + std::string name; + std::string description; + uint8_t serviceKind = Banker; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint8_t pad2 = 0; + uint32_t requiresGold = 0; // 1g = 10000c + uint32_t factionRequiredId = 0; // 0 = no faction gate + uint32_t gossipTextId = 0; // 0 = no custom dialogue + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t serviceId) const; + + // Return all services of a given kind across the + // catalog. Used by NPC-spawning code to find e.g. + // "all FlightMaster services configured for this + // server" when populating taxi nodes. + std::vector findByKind(uint8_t kind) const; + + static const char* serviceKindName(uint8_t k); +}; + +class WoweeNPCServiceLoader { +public: + static bool save(const WoweeNPCService& cat, + const std::string& basePath); + static WoweeNPCService load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-bkd* variants. + // + // makeCity — 5 city services (Banker / Mailbox / + // Innkeeper / Auctioneer / + // FlightMaster) typically present in + // a capital city like Stormwind or + // Orgrimmar. + // makeBattle — 3 battlemaster services (Alterac / + // Warsong / Arathi) for queueing into + // each Vanilla battleground. + // makeProfession — 4 profession services (Blacksmith + // Trainer / Tailoring Trainer / + // Reagent Vendor / Stable Master) + // typical of profession districts. + static WoweeNPCService makeCity(const std::string& catalogName); + static WoweeNPCService makeBattle(const std::string& catalogName); + static WoweeNPCService makeProfession(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_npc_services.cpp b/src/pipeline/wowee_npc_services.cpp new file mode 100644 index 00000000..2a394354 --- /dev/null +++ b/src/pipeline/wowee_npc_services.cpp @@ -0,0 +1,245 @@ +#include "pipeline/wowee_npc_services.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'B', 'K', 'D'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wbkd") { + base += ".wbkd"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeNPCService::Entry* +WoweeNPCService::findById(uint32_t serviceId) const { + for (const auto& e : entries) + if (e.serviceId == serviceId) return &e; + return nullptr; +} + +std::vector +WoweeNPCService::findByKind(uint8_t kind) const { + std::vector out; + for (const auto& e : entries) { + if (e.serviceKind == kind) out.push_back(&e); + } + return out; +} + +const char* WoweeNPCService::serviceKindName(uint8_t k) { + switch (k) { + case Banker: return "banker"; + case Mailbox: return "mailbox"; + case Auctioneer: return "auctioneer"; + case StableMaster: return "stable-master"; + case FlightMaster: return "flight-master"; + case Trainer: return "trainer"; + case Innkeeper: return "innkeeper"; + case Battlemaster: return "battlemaster"; + case GuildBanker: return "guild-banker"; + case ReagentVendor: return "reagent-vendor"; + case TabardVendor: return "tabard-vendor"; + case Misc: return "misc"; + default: return "unknown"; + } +} + +bool WoweeNPCServiceLoader::save(const WoweeNPCService& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.serviceId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.serviceKind); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.pad2); + writePOD(os, e.requiresGold); + writePOD(os, e.factionRequiredId); + writePOD(os, e.gossipTextId); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeNPCService WoweeNPCServiceLoader::load(const std::string& basePath) { + WoweeNPCService out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.serviceId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.serviceKind) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.pad2) || + !readPOD(is, e.requiresGold) || + !readPOD(is, e.factionRequiredId) || + !readPOD(is, e.gossipTextId) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeNPCServiceLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeNPCService WoweeNPCServiceLoader::makeCity( + const std::string& catalogName) { + using N = WoweeNPCService; + WoweeNPCService c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint32_t cop, uint32_t faction, uint32_t gossip, + uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + N::Entry e; + e.serviceId = id; e.name = name; e.description = desc; + e.serviceKind = kind; + e.requiresGold = cop; + e.factionRequiredId = faction; + e.gossipTextId = gossip; + e.iconColorRGBA = packRgba(r, g, b); + c.entries.push_back(e); + }; + add(1, "CityBanker", N::Banker, 0, 0, 1000, + 220, 220, 100, "City banker — opens 28-slot inventory bank."); + add(2, "CityMailbox", N::Mailbox, 0, 0, 0, + 180, 180, 240, "City mailbox — send/receive mail (no NPC)."); + add(3, "CityInnkeeper", N::Innkeeper, 0, 0, 1500, + 240, 200, 100, "City innkeeper — set hearthstone bind, " + "rest XP buff."); + add(4, "CityAuctioneer", N::Auctioneer, 0, 0, 1200, + 180, 220, 180, "City auctioneer — opens AH (5%% deposit, " + "5%% sale cut)."); + add(5, "CityFlightMaster",N::FlightMaster, 0, 0, 1100, + 140, 200, 240, "City flight master — taxi node selection."); + return c; +} + +WoweeNPCService WoweeNPCServiceLoader::makeBattle( + const std::string& catalogName) { + using N = WoweeNPCService; + WoweeNPCService c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, const char* desc) { + N::Entry e; + e.serviceId = id; e.name = name; e.description = desc; + e.serviceKind = N::Battlemaster; + // Battlemasters don't charge gold, but require + // faction-aligned battleground access. + e.iconColorRGBA = packRgba(220, 80, 80); // pvp red + c.entries.push_back(e); + }; + add(100, "BattlemasterAV", + "Alterac Valley battlemaster — 40v40 BG queue."); + add(101, "BattlemasterWSG", + "Warsong Gulch battlemaster — 10v10 capture-flag BG queue."); + add(102, "BattlemasterAB", + "Arathi Basin battlemaster — 15v15 control-point BG queue."); + return c; +} + +WoweeNPCService WoweeNPCServiceLoader::makeProfession( + const std::string& catalogName) { + using N = WoweeNPCService; + WoweeNPCService c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint32_t cop, const char* desc) { + N::Entry e; + e.serviceId = id; e.name = name; e.description = desc; + e.serviceKind = kind; + e.requiresGold = cop; + e.iconColorRGBA = packRgba(180, 140, 80); // crafting brown + c.entries.push_back(e); + }; + add(200, "BlacksmithTrainer", N::Trainer, 0, + "Blacksmithing trainer — teaches recipes and rank-ups."); + add(201, "TailoringTrainer", N::Trainer, 0, + "Tailoring trainer — teaches cloth crafting recipes."); + add(202, "ReagentVendor", N::ReagentVendor, 0, + "Reagent vendor — sells profession reagents in stacks."); + add(203, "StableMaster", N::StableMaster, 100, + "Stable master — costs 1 silver to swap pets in/out " + "of stable."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index ddb7f79c..92121aa7 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -266,6 +266,8 @@ const char* const kArgRequired[] = { "--gen-ifs", "--gen-ifs-binding", "--gen-ifs-server", "--info-wifs", "--validate-wifs", "--export-wifs-json", "--import-wifs-json", + "--gen-bkd", "--gen-bkd-battle", "--gen-bkd-profession", + "--info-wbkd", "--validate-wbkd", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index e49083f4..c6d5ccf2 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -129,6 +129,7 @@ #include "cli_item_qualities_catalog.hpp" #include "cli_skill_costs_catalog.hpp" #include "cli_item_flags_catalog.hpp" +#include "cli_npc_services_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -299,6 +300,7 @@ constexpr DispatchFn kDispatchTable[] = { handleItemQualitiesCatalog, handleSkillCostsCatalog, handleItemFlagsCatalog, + handleNPCServicesCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 5bbb8b6a..9d060ba9 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -89,6 +89,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','I','Q','R'}, ".wiqr", "items", "--info-wiqr", "Item quality tier catalog"}, {{'W','S','C','S'}, ".wscs", "skills", "--info-wscs", "Skill cost / training tier catalog"}, {{'W','I','F','S'}, ".wifs", "items", "--info-wifs", "Item flag bit catalog"}, + {{'W','B','K','D'}, ".wbkd", "npcs", "--info-wbkd", "NPC service definition catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 7f0655a5..6ce9aa89 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1967,6 +1967,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wifs to a human-editable JSON sidecar (defaults to .wifs.json)\n"); std::printf(" --import-wifs-json [out-base]\n"); std::printf(" Import a .wifs.json sidecar back into binary .wifs (accepts flagKind int OR name; isPositive bool OR int)\n"); + std::printf(" --gen-bkd [name]\n"); + std::printf(" Emit .wbkd 5 city services (Banker / Mailbox / Innkeeper / Auctioneer / FlightMaster) typical of capital city offerings\n"); + std::printf(" --gen-bkd-battle [name]\n"); + std::printf(" Emit .wbkd 3 battlemaster services (Alterac Valley / Warsong Gulch / Arathi Basin) for queueing into Vanilla BGs\n"); + std::printf(" --gen-bkd-profession [name]\n"); + std::printf(" Emit .wbkd 4 profession services (Blacksmith Trainer / Tailoring Trainer / Reagent Vendor / Stable Master)\n"); + std::printf(" --info-wbkd [--json]\n"); + std::printf(" Print WBKD entries (id / kind / gold cost / faction gate / gossip text id / name)\n"); + std::printf(" --validate-wbkd [--json]\n"); + std::printf(" Static checks: id+name required, serviceKind 0..11, no duplicate ids; warns on Mailbox+gossip (no NPC dialog), Innkeeper+no-gossip (silent bind), Battlemaster+gold (queues are free)\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 2e24772e..59411b55 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -111,6 +111,7 @@ constexpr FormatRow kFormats[] = { {"WIQR", ".wiqr", "items", "Item quality tier colors+rules", "Item quality tier catalog"}, {"WSCS", ".wscs", "skills", "SkillCostsData.dbc + train tiers", "Skill cost / training tier catalog"}, {"WIFS", ".wifs", "items", "Item.dbc Flags bit meanings", "Item flag bit catalog"}, + {"WBKD", ".wbkd", "npcs", "npc_vendor + npc_trainer SQL", "NPC service definition catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_npc_services_catalog.cpp b/tools/editor/cli_npc_services_catalog.cpp new file mode 100644 index 00000000..2dab86da --- /dev/null +++ b/tools/editor/cli_npc_services_catalog.cpp @@ -0,0 +1,242 @@ +#include "cli_npc_services_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_npc_services.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWbkdExt(std::string base) { + stripExt(base, ".wbkd"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeNPCService& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeNPCServiceLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wbkd\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeNPCService& c, + const std::string& base) { + std::printf("Wrote %s.wbkd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" services : %zu\n", c.entries.size()); +} + +int handleGenCity(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "CityServices"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWbkdExt(base); + auto c = wowee::pipeline::WoweeNPCServiceLoader::makeCity(name); + if (!saveOrError(c, base, "gen-bkd")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenBattle(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BattleServices"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWbkdExt(base); + auto c = wowee::pipeline::WoweeNPCServiceLoader::makeBattle(name); + if (!saveOrError(c, base, "gen-bkd-battle")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenProfession(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ProfessionServices"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWbkdExt(base); + auto c = wowee::pipeline::WoweeNPCServiceLoader::makeProfession(name); + if (!saveOrError(c, base, "gen-bkd-profession")) 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 = stripWbkdExt(base); + if (!wowee::pipeline::WoweeNPCServiceLoader::exists(base)) { + std::fprintf(stderr, "WBKD not found: %s.wbkd\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeNPCServiceLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wbkd"] = base + ".wbkd"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"serviceId", e.serviceId}, + {"name", e.name}, + {"description", e.description}, + {"serviceKind", e.serviceKind}, + {"serviceKindName", wowee::pipeline::WoweeNPCService::serviceKindName(e.serviceKind)}, + {"requiresGold", e.requiresGold}, + {"factionRequiredId", e.factionRequiredId}, + {"gossipTextId", e.gossipTextId}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WBKD: %s.wbkd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" services : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind gold faction gossip name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-13s %6u %5u %5u %s\n", + e.serviceId, + wowee::pipeline::WoweeNPCService::serviceKindName(e.serviceKind), + e.requiresGold, e.factionRequiredId, + e.gossipTextId, e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWbkdExt(base); + if (!wowee::pipeline::WoweeNPCServiceLoader::exists(base)) { + std::fprintf(stderr, + "validate-wbkd: WBKD not found: %s.wbkd\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeNPCServiceLoader::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.serviceId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.serviceId == 0) + errors.push_back(ctx + ": serviceId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.serviceKind > wowee::pipeline::WoweeNPCService::Misc) { + errors.push_back(ctx + ": serviceKind " + + std::to_string(e.serviceKind) + " not in 0..11"); + } + // Mailbox is a gameobject service, not an NPC — + // shouldn't have a gossipTextId. Warn so authors + // double-check. + if (e.serviceKind == wowee::pipeline::WoweeNPCService::Mailbox && + e.gossipTextId != 0) { + warnings.push_back(ctx + + ": Mailbox kind with gossipTextId=" + + std::to_string(e.gossipTextId) + + " — mailboxes are gameobject services with " + "no NPC dialogue; gossip will not display"); + } + // Innkeeper without a gossip text reads as a silent + // bind interaction — usually a missing link. + if (e.serviceKind == wowee::pipeline::WoweeNPCService::Innkeeper && + e.gossipTextId == 0) { + warnings.push_back(ctx + + ": Innkeeper kind with gossipTextId=0 — " + "no welcome/bind dialogue, will silently bind"); + } + // Battlemaster gold cost > 0 is unusual — battle + // queues are typically free. + if (e.serviceKind == wowee::pipeline::WoweeNPCService::Battlemaster && + e.requiresGold > 0) { + warnings.push_back(ctx + + ": Battlemaster kind with requiresGold=" + + std::to_string(e.requiresGold) + + " — battle queue services are typically free"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.serviceId) { + errors.push_back(ctx + ": duplicate serviceId"); + break; + } + } + idsSeen.push_back(e.serviceId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wbkd"] = base + ".wbkd"; + 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-wbkd: %s.wbkd\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu services, all serviceIds 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 handleNPCServicesCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-bkd") == 0 && i + 1 < argc) { + outRc = handleGenCity(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-bkd-battle") == 0 && i + 1 < argc) { + outRc = handleGenBattle(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-bkd-profession") == 0 && i + 1 < argc) { + outRc = handleGenProfession(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wbkd") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wbkd") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_npc_services_catalog.hpp b/tools/editor/cli_npc_services_catalog.hpp new file mode 100644 index 00000000..827bdeb7 --- /dev/null +++ b/tools/editor/cli_npc_services_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleNPCServicesCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee