From f98157b5a579ee6da0d5a3a7ca5892c4421e68f5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 20:05:06 -0700 Subject: [PATCH] feat(pipeline): add WCMP (Wowee Companion Pet) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 53rd open format — replaces the companion-pet portions of CreatureFamily.dbc plus the AzerothCore-style critter / vanity- pet SQL data. Distinct from WPET (which covers hunter combat pets and warlock minions); WCMP covers non-combat "vanity" pets that follow the player around for cosmetic reasons — Mechanical Squirrel, Mini Diablo, Panda Cub, dragon hatchlings. 8 companion kinds (Critter, Mechanical, DragonHatchling, Demonic, Spectral, Elemental, Plush, UndeadCritter), 4 rarity tiers (Common / Uncommon / Rare / Epic), and 3 faction restrictions (AnyFaction / AllianceOnly / HordeOnly). Cross-references with prior formats — creatureId points at WCRT.creatureId (the rendered model), learnSpellId at WSPL.spellId (the spell that summons the pet), itemId at WIT.itemId (the item that teaches the spell), and idleSoundId at WSND.soundId (idle ambient noise). CLI: --gen-cmp (3 common vendor pets), --gen-cmp-rare (4 promo / collector pets at Epic rarity — Mini Diablo, Panda Cub, Zergling, Murky), --gen-cmp-faction (3 faction- restricted Alliance Lion Cub / Horde Mottled Boar / neutral Argent Squire), --info-wcmp, --validate-wcmp with --json variants. Validator catches id+name+creatureId+ learnSpellId required, kind 0..7 / rarity 0..3 / faction 0..2 range, and Epic-rarity-no-itemId warning (most promo pets need a redemption-code item). Format graph: 52 → 53 binary formats. CLI flag count: 777 → 782. --- CMakeLists.txt | 3 + include/pipeline/wowee_companions.hpp | 123 +++++++++++ src/pipeline/wowee_companions.cpp | 264 ++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_companions_catalog.cpp | 243 ++++++++++++++++++++++ tools/editor/cli_companions_catalog.hpp | 11 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 660 insertions(+) create mode 100644 include/pipeline/wowee_companions.hpp create mode 100644 src/pipeline/wowee_companions.cpp create mode 100644 tools/editor/cli_companions_catalog.cpp create mode 100644 tools/editor/cli_companions_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b5b2cd49..623fc6bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -641,6 +641,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_creature_equipment.cpp src/pipeline/wowee_item_sets.cpp src/pipeline/wowee_game_tips.cpp + src/pipeline/wowee_companions.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1434,6 +1435,7 @@ add_executable(wowee_editor tools/editor/cli_item_sets_catalog.cpp tools/editor/cli_touch_tree.cpp tools/editor/cli_game_tips_catalog.cpp + tools/editor/cli_companions_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1553,6 +1555,7 @@ add_executable(wowee_editor src/pipeline/wowee_creature_equipment.cpp src/pipeline/wowee_item_sets.cpp src/pipeline/wowee_game_tips.cpp + src/pipeline/wowee_companions.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_companions.hpp b/include/pipeline/wowee_companions.hpp new file mode 100644 index 00000000..340e5e8a --- /dev/null +++ b/include/pipeline/wowee_companions.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Companion Pet catalog (.wcmp) — novel +// replacement for the companion-pet portions of CreatureFamily +// .dbc plus the AzerothCore-style critter / vanity-pet SQL +// data. Distinct from WPET (which covers hunter combat pets +// and warlock minions); this format covers non-combat +// "vanity" pets that follow the player around for cosmetic +// reasons — Mechanical Squirrel, Mini Diablo, Panda Cub, +// dragon hatchlings, etc. +// +// Each companion binds a creature template (the rendered +// model) to the spell that summons it, and optionally to the +// item that teaches the spell when used. companionKind groups +// them by visual archetype for filter / collection display; +// rarity drives drop chance and collection-display sort. +// +// Cross-references with previously-added formats: +// WCMP.entry.creatureId → WCRT.creatureId +// WCMP.entry.learnSpellId → WSPL.spellId +// WCMP.entry.itemId → WIT.itemId (teaches the spell) +// WCMP.entry.idleSoundId → WSND.soundId (ambient noise) +// +// Binary layout (little-endian): +// magic[4] = "WCMP" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// companionId (uint32) +// creatureId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// companionKind (uint8) / rarity (uint8) / +// factionRestriction (uint8) / pad[1] +// learnSpellId (uint32) +// itemId (uint32) +// idleSoundId (uint32) +struct WoweeCompanion { + enum CompanionKind : uint8_t { + Critter = 0, // small mundane animals + Mechanical = 1, // engineered constructs + DragonHatchling = 2, // small dragons / drakes + Demonic = 3, // imps / felguard pups + Spectral = 4, // ghosts / wisps + Elemental = 5, // fire / water / earth / air sprites + Plush = 6, // promotional toys / collectibles + UndeadCritter = 7, // skeletal pets / ghouls + }; + + enum Rarity : uint8_t { + Common = 0, // vendor / common drop + Uncommon = 1, // dungeon drop / faction quartermaster + Rare = 2, // raid / promo + Epic = 3, // legendary / Blizzcon promo + }; + + enum FactionRestriction : uint8_t { + AnyFaction = 0, + AllianceOnly = 1, + HordeOnly = 2, + }; + + struct Entry { + uint32_t companionId = 0; + uint32_t creatureId = 0; // WCRT cross-ref + std::string name; + std::string description; + std::string iconPath; + uint8_t companionKind = Critter; + uint8_t rarity = Common; + uint8_t factionRestriction = AnyFaction; + uint32_t learnSpellId = 0; // WSPL cross-ref + uint32_t itemId = 0; // WIT cross-ref (optional) + uint32_t idleSoundId = 0; // WSND cross-ref (optional) + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t companionId) const; + + static const char* companionKindName(uint8_t k); + static const char* rarityName(uint8_t r); + static const char* factionRestrictionName(uint8_t f); +}; + +class WoweeCompanionLoader { +public: + static bool save(const WoweeCompanion& cat, + const std::string& basePath); + static WoweeCompanion load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-cmp* variants. + // + // makeStarter — 3 common vendor-bought companions + // (Mechanical Squirrel / Cat / Prairie + // Dog) — Common rarity, no faction gate. + // makeRare — 4 rare promo / collector pets (Mini + // Diablo / Panda Cub / Zergling / Murky) + // with Epic / Rare rarity. + // makeFaction — 3 faction-specific (Alliance Lion Cub / + // Horde Mottled Boar / Argent Squire) + // using AllianceOnly + HordeOnly + + // AnyFaction. + static WoweeCompanion makeStarter(const std::string& catalogName); + static WoweeCompanion makeRare(const std::string& catalogName); + static WoweeCompanion makeFaction(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_companions.cpp b/src/pipeline/wowee_companions.cpp new file mode 100644 index 00000000..0316f046 --- /dev/null +++ b/src/pipeline/wowee_companions.cpp @@ -0,0 +1,264 @@ +#include "pipeline/wowee_companions.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'C', 'M', 'P'}; +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) != ".wcmp") { + base += ".wcmp"; + } + return base; +} + +} // namespace + +const WoweeCompanion::Entry* +WoweeCompanion::findById(uint32_t companionId) const { + for (const auto& e : entries) + if (e.companionId == companionId) return &e; + return nullptr; +} + +const char* WoweeCompanion::companionKindName(uint8_t k) { + switch (k) { + case Critter: return "critter"; + case Mechanical: return "mechanical"; + case DragonHatchling: return "dragon"; + case Demonic: return "demonic"; + case Spectral: return "spectral"; + case Elemental: return "elemental"; + case Plush: return "plush"; + case UndeadCritter: return "undead"; + default: return "unknown"; + } +} + +const char* WoweeCompanion::rarityName(uint8_t r) { + switch (r) { + case Common: return "common"; + case Uncommon: return "uncommon"; + case Rare: return "rare"; + case Epic: return "epic"; + default: return "unknown"; + } +} + +const char* WoweeCompanion::factionRestrictionName(uint8_t f) { + switch (f) { + case AnyFaction: return "any"; + case AllianceOnly: return "alliance"; + case HordeOnly: return "horde"; + default: return "unknown"; + } +} + +bool WoweeCompanionLoader::save(const WoweeCompanion& 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.companionId); + writePOD(os, e.creatureId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.iconPath); + writePOD(os, e.companionKind); + writePOD(os, e.rarity); + writePOD(os, e.factionRestriction); + uint8_t pad = 0; + writePOD(os, pad); + writePOD(os, e.learnSpellId); + writePOD(os, e.itemId); + writePOD(os, e.idleSoundId); + } + return os.good(); +} + +WoweeCompanion WoweeCompanionLoader::load(const std::string& basePath) { + WoweeCompanion 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.companionId) || + !readPOD(is, e.creatureId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.iconPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.companionKind) || + !readPOD(is, e.rarity) || + !readPOD(is, e.factionRestriction)) { + out.entries.clear(); return out; + } + uint8_t pad = 0; + if (!readPOD(is, pad)) { out.entries.clear(); return out; } + if (!readPOD(is, e.learnSpellId) || + !readPOD(is, e.itemId) || + !readPOD(is, e.idleSoundId)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeCompanionLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeCompanion WoweeCompanionLoader::makeStarter( + const std::string& catalogName) { + WoweeCompanion c; + c.name = catalogName; + auto add = [&](uint32_t id, uint32_t creature, const char* name, + uint32_t spellId, uint32_t itemId, + uint8_t kind, const char* desc) { + WoweeCompanion::Entry e; + e.companionId = id; e.creatureId = creature; + e.name = name; e.description = desc; + e.iconPath = std::string("Interface/Icons/Inv_Pet_") + + name + ".blp"; + e.companionKind = kind; + e.rarity = WoweeCompanion::Common; + e.learnSpellId = spellId; + e.itemId = itemId; + c.entries.push_back(e); + }; + add(1, 7560, "MechanicalSquirrel", 4055, 4401, + WoweeCompanion::Mechanical, + "Engineering-built mechanical squirrel — clicks " + "and chitters as it follows."); + add(2, 7349, "Cat", 10684, 8491, + WoweeCompanion::Critter, + "Generic alley cat — purrs when stationary."); + add(3, 7547, "PrairieDog", 9484, 7560, + WoweeCompanion::Critter, + "Tan prairie dog — pops up to look around " + "every few seconds."); + return c; +} + +WoweeCompanion WoweeCompanionLoader::makeRare( + const std::string& catalogName) { + WoweeCompanion c; + c.name = catalogName; + auto add = [&](uint32_t id, uint32_t creature, const char* name, + uint32_t spellId, uint32_t itemId, + uint8_t kind, uint8_t rarity, const char* desc) { + WoweeCompanion::Entry e; + e.companionId = id; e.creatureId = creature; + e.name = name; e.description = desc; + e.iconPath = std::string("Interface/Icons/Inv_Pet_") + + name + ".blp"; + e.companionKind = kind; + e.rarity = rarity; + e.learnSpellId = spellId; + e.itemId = itemId; + c.entries.push_back(e); + }; + add(100, 11325, "MiniDiablo", 23147, 18639, + WoweeCompanion::Demonic, WoweeCompanion::Epic, + "Promotional Diablo II tie-in pet."); + add(101, 11326, "PandaCub", 23163, 18646, + WoweeCompanion::Critter, WoweeCompanion::Epic, + "Collector's edition panda cub."); + add(102, 11327, "Zergling", 23161, 18647, + WoweeCompanion::Critter, WoweeCompanion::Epic, + "Promotional StarCraft tie-in pet."); + add(103, 16599, "Murky", 25746, 21337, + WoweeCompanion::Critter, WoweeCompanion::Epic, + "Blizzcon 2005 promotional baby murloc."); + return c; +} + +WoweeCompanion WoweeCompanionLoader::makeFaction( + const std::string& catalogName) { + WoweeCompanion c; + c.name = catalogName; + auto add = [&](uint32_t id, uint32_t creature, const char* name, + uint32_t spellId, uint32_t itemId, + uint8_t kind, uint8_t faction, const char* desc) { + WoweeCompanion::Entry e; + e.companionId = id; e.creatureId = creature; + e.name = name; e.description = desc; + e.iconPath = std::string("Interface/Icons/Inv_Pet_") + + name + ".blp"; + e.companionKind = kind; + e.rarity = WoweeCompanion::Uncommon; + e.factionRestriction = faction; + e.learnSpellId = spellId; + e.itemId = itemId; + c.entries.push_back(e); + }; + add(200, 17254, "AllianceLionCub", 29726, 23713, + WoweeCompanion::Critter, WoweeCompanion::AllianceOnly, + "Stormwind orphan-week reward — Alliance only."); + add(201, 17255, "HordeMottledBoar", 29727, 23714, + WoweeCompanion::Critter, WoweeCompanion::HordeOnly, + "Orgrimmar orphan-week reward — Horde only."); + add(202, 33272, "ArgentSquire", 54187, 39286, + WoweeCompanion::Critter, WoweeCompanion::AnyFaction, + "Argent Tournament squire — any faction may purchase."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 93950b01..31187481 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -160,6 +160,8 @@ const char* const kArgRequired[] = { "--gen-tips", "--gen-tips-new-player", "--gen-tips-advanced", "--info-wgtp", "--validate-wgtp", "--export-wgtp-json", "--import-wgtp-json", + "--gen-cmp", "--gen-cmp-rare", "--gen-cmp-faction", + "--info-wcmp", "--validate-wcmp", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_companions_catalog.cpp b/tools/editor/cli_companions_catalog.cpp new file mode 100644 index 00000000..3a240f9d --- /dev/null +++ b/tools/editor/cli_companions_catalog.cpp @@ -0,0 +1,243 @@ +#include "cli_companions_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_companions.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWcmpExt(std::string base) { + stripExt(base, ".wcmp"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeCompanion& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeCompanionLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wcmp\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeCompanion& c, + const std::string& base) { + std::printf("Wrote %s.wcmp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" companions : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterCompanions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmpExt(base); + auto c = wowee::pipeline::WoweeCompanionLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-cmp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRare(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RareCompanions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmpExt(base); + auto c = wowee::pipeline::WoweeCompanionLoader::makeRare(name); + if (!saveOrError(c, base, "gen-cmp-rare")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenFaction(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FactionCompanions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmpExt(base); + auto c = wowee::pipeline::WoweeCompanionLoader::makeFaction(name); + if (!saveOrError(c, base, "gen-cmp-faction")) 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 = stripWcmpExt(base); + if (!wowee::pipeline::WoweeCompanionLoader::exists(base)) { + std::fprintf(stderr, "WCMP not found: %s.wcmp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCompanionLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wcmp"] = base + ".wcmp"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"companionId", e.companionId}, + {"creatureId", e.creatureId}, + {"name", e.name}, + {"description", e.description}, + {"iconPath", e.iconPath}, + {"companionKind", e.companionKind}, + {"companionKindName", wowee::pipeline::WoweeCompanion::companionKindName(e.companionKind)}, + {"rarity", e.rarity}, + {"rarityName", wowee::pipeline::WoweeCompanion::rarityName(e.rarity)}, + {"factionRestriction", e.factionRestriction}, + {"factionRestrictionName", wowee::pipeline::WoweeCompanion::factionRestrictionName(e.factionRestriction)}, + {"learnSpellId", e.learnSpellId}, + {"itemId", e.itemId}, + {"idleSoundId", e.idleSoundId}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCMP: %s.wcmp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" companions : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id creature kind rarity faction spell item sound name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %-10s %-8s %-8s %5u %5u %5u %s\n", + e.companionId, e.creatureId, + wowee::pipeline::WoweeCompanion::companionKindName(e.companionKind), + wowee::pipeline::WoweeCompanion::rarityName(e.rarity), + wowee::pipeline::WoweeCompanion::factionRestrictionName(e.factionRestriction), + e.learnSpellId, e.itemId, e.idleSoundId, + 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 = stripWcmpExt(base); + if (!wowee::pipeline::WoweeCompanionLoader::exists(base)) { + std::fprintf(stderr, + "validate-wcmp: WCMP not found: %s.wcmp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCompanionLoader::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.companionId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.companionId == 0) + errors.push_back(ctx + ": companionId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.creatureId == 0) + errors.push_back(ctx + + ": creatureId is 0 (companion has no rendered model)"); + if (e.learnSpellId == 0) + errors.push_back(ctx + + ": learnSpellId is 0 (no spell to summon companion)"); + if (e.companionKind > wowee::pipeline::WoweeCompanion::UndeadCritter) { + errors.push_back(ctx + ": companionKind " + + std::to_string(e.companionKind) + " not in 0..7"); + } + if (e.rarity > wowee::pipeline::WoweeCompanion::Epic) { + errors.push_back(ctx + ": rarity " + + std::to_string(e.rarity) + " not in 0..3"); + } + if (e.factionRestriction > wowee::pipeline::WoweeCompanion::HordeOnly) { + errors.push_back(ctx + ": factionRestriction " + + std::to_string(e.factionRestriction) + " not in 0..2"); + } + // Epic rarity without an itemId is unusual — promo + // pets typically have a redemption code item or + // collector's edition box. + if (e.rarity == wowee::pipeline::WoweeCompanion::Epic && + e.itemId == 0) { + warnings.push_back(ctx + + ": Epic rarity but itemId=0 (no source item — " + "verify intentional for code-only redemption)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.companionId) { + errors.push_back(ctx + ": duplicate companionId"); + break; + } + } + idsSeen.push_back(e.companionId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wcmp"] = base + ".wcmp"; + 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-wcmp: %s.wcmp\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu companions, all companionIds 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 handleCompanionsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-cmp") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-cmp-rare") == 0 && i + 1 < argc) { + outRc = handleGenRare(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-cmp-faction") == 0 && i + 1 < argc) { + outRc = handleGenFaction(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wcmp") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wcmp") == 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_companions_catalog.hpp b/tools/editor/cli_companions_catalog.hpp new file mode 100644 index 00000000..fca05504 --- /dev/null +++ b/tools/editor/cli_companions_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleCompanionsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index ade533ac..efa5ea93 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -86,6 +86,7 @@ #include "cli_item_sets_catalog.hpp" #include "cli_touch_tree.hpp" #include "cli_game_tips_catalog.hpp" +#include "cli_companions_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -213,6 +214,7 @@ constexpr DispatchFn kDispatchTable[] = { handleItemSetsCatalog, handleTouchTree, handleGameTipsCatalog, + handleCompanionsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index e56e94a9..65dd9a7f 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -55,6 +55,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','C','E','Q'}, ".wceq", "creatures", "--info-wceq", "Creature equipment loadout catalog"}, {{'W','S','E','T'}, ".wset", "items", "--info-wset", "Item set / tier-bonus catalog"}, {{'W','G','T','P'}, ".wgtp", "ui", "--info-wgtp", "Game tips / tutorial catalog"}, + {{'W','C','M','P'}, ".wcmp", "pets", "--info-wcmp", "Companion / vanity pet 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 38b0b2bf..fb47e3ea 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1471,6 +1471,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wgtp to a human-editable JSON sidecar (defaults to .wgtp.json)\n"); std::printf(" --import-wgtp-json [out-base]\n"); std::printf(" Import a .wgtp.json sidecar back into binary .wgtp (accepts displayKind int OR name string)\n"); + std::printf(" --gen-cmp [name]\n"); + std::printf(" Emit .wcmp starter: 3 common vendor companions (Mechanical Squirrel / Cat / Prairie Dog)\n"); + std::printf(" --gen-cmp-rare [name]\n"); + std::printf(" Emit .wcmp 4 promo / collector pets (Mini Diablo / Panda Cub / Zergling / Murky) at Epic rarity\n"); + std::printf(" --gen-cmp-faction [name]\n"); + std::printf(" Emit .wcmp 3 faction-restricted pets (Alliance Lion Cub / Horde Mottled Boar / Argent Squire any-faction)\n"); + std::printf(" --info-wcmp [--json]\n"); + std::printf(" Print WCMP entries (id / creature / kind / rarity / faction / learn spell+item / idle sound / name)\n"); + std::printf(" --validate-wcmp [--json]\n"); + std::printf(" Static checks: id+name+creatureId+learnSpellId required, kind 0..7, rarity 0..3, faction 0..2, Epic-no-itemId warning\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 a928d508..b7dd8199 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -77,6 +77,7 @@ constexpr FormatRow kFormats[] = { {"WCEQ", ".wceq", "creatures", "creature_equip_template", "Creature equipment loadout (visible weapons)"}, {"WSET", ".wset", "items", "ItemSet.dbc + ItemSetSpell.dbc", "Item set + tier-bonus catalog"}, {"WGTP", ".wgtp", "ui", "GameTips.dbc + tutorial hints", "Game tips / tutorial / loading-screen catalog"}, + {"WCMP", ".wcmp", "pets", "CreatureFamily + companion SQL", "Companion / vanity pet catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine