diff --git a/CMakeLists.txt b/CMakeLists.txt index 5db0dabe..b3e74e86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -614,6 +614,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_taxi.cpp src/pipeline/wowee_talents.cpp src/pipeline/wowee_maps.cpp + src/pipeline/wowee_chars.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1374,6 +1375,7 @@ add_executable(wowee_editor tools/editor/cli_taxi_catalog.cpp tools/editor/cli_talents_catalog.cpp tools/editor/cli_maps_catalog.cpp + tools/editor/cli_chars_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1466,6 +1468,7 @@ add_executable(wowee_editor src/pipeline/wowee_taxi.cpp src/pipeline/wowee_talents.cpp src/pipeline/wowee_maps.cpp + src/pipeline/wowee_chars.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_chars.hpp b/include/pipeline/wowee_chars.hpp new file mode 100644 index 00000000..3a4616ba --- /dev/null +++ b/include/pipeline/wowee_chars.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Character Classes/Races catalog (.wchc) — +// novel replacement for Blizzard's CharClasses.dbc + +// CharRaces.dbc + CharStartOutfit.dbc trio. The 27th open +// format added to the editor. +// +// Defines every player class, race, and the starting outfit +// (gear loadout) for each class+race+gender combination. +// One file holds three flat arrays: classes / races / +// outfits. +// +// Cross-references with previously-added formats: +// WCHC.race.startingMapId → WMS.map.mapId +// WCHC.race.startingZoneAreaId → WMS.area.areaId +// WCHC.race.defaultLanguageSpellId → WSPL.entry.spellId +// WCHC.race.mountSpellId → WSPL.entry.spellId +// WCHC.outfit.items.itemId → WIT.entry.itemId +// WCHC.class.canTrainProfessions → WSKL.entry.skillId +// (bitset by category) +// +// Binary layout (little-endian): +// magic[4] = "WCHC" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// classCount (uint32) + classes[] +// raceCount (uint32) + races[] +// outfitCount (uint32) + outfits[] +struct WoweeChars { + enum PowerType : uint8_t { + Mana = 0, + Rage = 1, + Focus = 2, + Energy = 3, + RunicPower = 4, + Runes = 6, // DK only + }; + + enum FactionAvailability : uint8_t { + AvailableAlliance = 0x01, + AvailableHorde = 0x02, + }; + + enum RaceFaction : uint8_t { + Alliance = 0, + Horde = 1, + Neutral = 2, // Pandaren start neutral + }; + + enum Gender : uint8_t { + Male = 0, + Female = 1, + }; + + struct Class { + uint32_t classId = 0; + std::string name; + std::string iconPath; + uint8_t powerType = Mana; + uint8_t displayPower = Mana; // can differ from powerType (druid) + uint32_t baseHealth = 50; + uint16_t baseHealthPerLevel = 12; + uint32_t basePower = 100; + uint16_t basePowerPerLevel = 5; + uint8_t factionAvailability = + AvailableAlliance | AvailableHorde; + }; + + struct Race { + uint32_t raceId = 0; + std::string name; + std::string iconPath; + uint8_t factionId = Alliance; + uint32_t maleDisplayId = 0; + uint32_t femaleDisplayId = 0; + uint16_t baseStrength = 20; + uint16_t baseAgility = 20; + uint16_t baseStamina = 20; + uint16_t baseIntellect = 20; + uint16_t baseSpirit = 20; + uint32_t startingMapId = 0; + uint32_t startingZoneAreaId = 0; + uint32_t defaultLanguageSpellId = 0; // 0 = none + uint32_t mountSpellId = 0; // racial mount spell + }; + + struct OutfitItem { + uint32_t itemId = 0; + uint8_t displaySlot = 0; // matches WIT.inventoryType + }; + + struct Outfit { + uint32_t classId = 0; + uint32_t raceId = 0; + uint8_t gender = Male; + std::vector items; + }; + + std::string name; + std::vector classes; + std::vector races; + std::vector outfits; + + bool isValid() const { return !classes.empty() || !races.empty(); } + + const Class* findClass(uint32_t classId) const; + const Race* findRace(uint32_t raceId) const; + // First outfit matching the (class, race, gender) triple, or nullptr. + const Outfit* findOutfit(uint32_t classId, uint32_t raceId, + uint8_t gender) const; + + static const char* powerTypeName(uint8_t p); + static const char* raceFactionName(uint8_t f); +}; + +class WoweeCharsLoader { +public: + static bool save(const WoweeChars& cat, + const std::string& basePath); + static WoweeChars load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-chars* variants. + // + // makeStarter — 2 classes (Warrior + Mage) + 2 races + // (Human Alliance + Orc Horde) + 4 outfits + // (2 classes × 2 races, male-only). + // makeAlliance — full Alliance faction: 4 classes + 4 + // races + 8 outfits. + // makeAllRaces — 8 classic playable races (Human / Dwarf + // / NightElf / Gnome on Alliance side; + // Orc / Undead / Tauren / Troll on Horde) + // plus 9 classes (no DK). + static WoweeChars makeStarter(const std::string& catalogName); + static WoweeChars makeAlliance(const std::string& catalogName); + static WoweeChars makeAllRaces(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_chars.cpp b/src/pipeline/wowee_chars.cpp new file mode 100644 index 00000000..16b50e46 --- /dev/null +++ b/src/pipeline/wowee_chars.cpp @@ -0,0 +1,418 @@ +#include "pipeline/wowee_chars.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'C', 'H', 'C'}; +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) != ".wchc") { + base += ".wchc"; + } + return base; +} + +} // namespace + +const WoweeChars::Class* WoweeChars::findClass(uint32_t classId) const { + for (const auto& c : classes) if (c.classId == classId) return &c; + return nullptr; +} + +const WoweeChars::Race* WoweeChars::findRace(uint32_t raceId) const { + for (const auto& r : races) if (r.raceId == raceId) return &r; + return nullptr; +} + +const WoweeChars::Outfit* WoweeChars::findOutfit(uint32_t classId, + uint32_t raceId, + uint8_t gender) const { + for (const auto& o : outfits) { + if (o.classId == classId && o.raceId == raceId && + o.gender == gender) return &o; + } + return nullptr; +} + +const char* WoweeChars::powerTypeName(uint8_t p) { + switch (p) { + case Mana: return "mana"; + case Rage: return "rage"; + case Focus: return "focus"; + case Energy: return "energy"; + case RunicPower: return "runic-power"; + case Runes: return "runes"; + default: return "unknown"; + } +} + +const char* WoweeChars::raceFactionName(uint8_t f) { + switch (f) { + case Alliance: return "alliance"; + case Horde: return "horde"; + case Neutral: return "neutral"; + default: return "unknown"; + } +} + +bool WoweeCharsLoader::save(const WoweeChars& 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 classCount = static_cast(cat.classes.size()); + writePOD(os, classCount); + for (const auto& c : cat.classes) { + writePOD(os, c.classId); + writeStr(os, c.name); + writeStr(os, c.iconPath); + writePOD(os, c.powerType); + writePOD(os, c.displayPower); + writePOD(os, c.factionAvailability); + uint8_t pad1 = 0; + writePOD(os, pad1); + writePOD(os, c.baseHealth); + writePOD(os, c.baseHealthPerLevel); + writePOD(os, c.basePower); + writePOD(os, c.basePowerPerLevel); + } + uint32_t raceCount = static_cast(cat.races.size()); + writePOD(os, raceCount); + for (const auto& r : cat.races) { + writePOD(os, r.raceId); + writeStr(os, r.name); + writeStr(os, r.iconPath); + writePOD(os, r.factionId); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, r.maleDisplayId); + writePOD(os, r.femaleDisplayId); + writePOD(os, r.baseStrength); + writePOD(os, r.baseAgility); + writePOD(os, r.baseStamina); + writePOD(os, r.baseIntellect); + writePOD(os, r.baseSpirit); + os.write(reinterpret_cast(pad3), 2); + writePOD(os, r.startingMapId); + writePOD(os, r.startingZoneAreaId); + writePOD(os, r.defaultLanguageSpellId); + writePOD(os, r.mountSpellId); + } + uint32_t outfitCount = static_cast(cat.outfits.size()); + writePOD(os, outfitCount); + for (const auto& o : cat.outfits) { + writePOD(os, o.classId); + writePOD(os, o.raceId); + writePOD(os, o.gender); + uint8_t itemCount = static_cast( + o.items.size() > 255 ? 255 : o.items.size()); + writePOD(os, itemCount); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + for (uint8_t k = 0; k < itemCount; ++k) { + const auto& it = o.items[k]; + writePOD(os, it.itemId); + writePOD(os, it.displaySlot); + uint8_t ipad[3] = {0, 0, 0}; + os.write(reinterpret_cast(ipad), 3); + } + } + return os.good(); +} + +WoweeChars WoweeCharsLoader::load(const std::string& basePath) { + WoweeChars 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 classCount = 0; + if (!readPOD(is, classCount)) return out; + if (classCount > (1u << 20)) return out; + out.classes.resize(classCount); + for (auto& c : out.classes) { + if (!readPOD(is, c.classId)) { out.classes.clear(); return out; } + if (!readStr(is, c.name) || !readStr(is, c.iconPath)) { + out.classes.clear(); return out; + } + if (!readPOD(is, c.powerType) || + !readPOD(is, c.displayPower) || + !readPOD(is, c.factionAvailability)) { + out.classes.clear(); return out; + } + uint8_t pad1 = 0; + if (!readPOD(is, pad1)) { + out.classes.clear(); return out; + } + if (!readPOD(is, c.baseHealth) || + !readPOD(is, c.baseHealthPerLevel) || + !readPOD(is, c.basePower) || + !readPOD(is, c.basePowerPerLevel)) { + out.classes.clear(); return out; + } + } + uint32_t raceCount = 0; + if (!readPOD(is, raceCount)) { + out.classes.clear(); return out; + } + if (raceCount > (1u << 20)) { + out.classes.clear(); return out; + } + out.races.resize(raceCount); + for (auto& r : out.races) { + if (!readPOD(is, r.raceId)) { + out.classes.clear(); out.races.clear(); return out; + } + if (!readStr(is, r.name) || !readStr(is, r.iconPath)) { + out.classes.clear(); out.races.clear(); return out; + } + if (!readPOD(is, r.factionId)) { + out.classes.clear(); out.races.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { + out.classes.clear(); out.races.clear(); return out; + } + if (!readPOD(is, r.maleDisplayId) || + !readPOD(is, r.femaleDisplayId) || + !readPOD(is, r.baseStrength) || + !readPOD(is, r.baseAgility) || + !readPOD(is, r.baseStamina) || + !readPOD(is, r.baseIntellect) || + !readPOD(is, r.baseSpirit)) { + out.classes.clear(); out.races.clear(); return out; + } + is.read(reinterpret_cast(pad3), 2); + if (is.gcount() != 2) { + out.classes.clear(); out.races.clear(); return out; + } + if (!readPOD(is, r.startingMapId) || + !readPOD(is, r.startingZoneAreaId) || + !readPOD(is, r.defaultLanguageSpellId) || + !readPOD(is, r.mountSpellId)) { + out.classes.clear(); out.races.clear(); return out; + } + } + uint32_t outfitCount = 0; + if (!readPOD(is, outfitCount)) { + out.classes.clear(); out.races.clear(); return out; + } + if (outfitCount > (1u << 20)) { + out.classes.clear(); out.races.clear(); return out; + } + out.outfits.resize(outfitCount); + for (auto& o : out.outfits) { + if (!readPOD(is, o.classId) || + !readPOD(is, o.raceId) || + !readPOD(is, o.gender)) { + out.classes.clear(); out.races.clear(); + out.outfits.clear(); return out; + } + uint8_t itemCount = 0; + if (!readPOD(is, itemCount)) { + out.classes.clear(); out.races.clear(); + out.outfits.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { + out.classes.clear(); out.races.clear(); + out.outfits.clear(); return out; + } + o.items.resize(itemCount); + for (uint8_t k = 0; k < itemCount; ++k) { + auto& it = o.items[k]; + if (!readPOD(is, it.itemId) || + !readPOD(is, it.displaySlot)) { + out.classes.clear(); out.races.clear(); + out.outfits.clear(); return out; + } + uint8_t ipad[3]; + is.read(reinterpret_cast(ipad), 3); + if (is.gcount() != 3) { + out.classes.clear(); out.races.clear(); + out.outfits.clear(); return out; + } + } + } + return out; +} + +bool WoweeCharsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeChars WoweeCharsLoader::makeStarter(const std::string& catalogName) { + WoweeChars c; + c.name = catalogName; + { + WoweeChars::Class cls; + cls.classId = 1; cls.name = "Warrior"; + cls.powerType = WoweeChars::Rage; cls.displayPower = WoweeChars::Rage; + cls.baseHealth = 60; cls.baseHealthPerLevel = 18; + cls.basePower = 100; cls.basePowerPerLevel = 0; + c.classes.push_back(cls); + } + { + WoweeChars::Class cls; + cls.classId = 8; cls.name = "Mage"; + cls.powerType = WoweeChars::Mana; cls.displayPower = WoweeChars::Mana; + cls.baseHealth = 35; cls.baseHealthPerLevel = 10; + cls.basePower = 100; cls.basePowerPerLevel = 30; + c.classes.push_back(cls); + } + { + WoweeChars::Race r; + r.raceId = 1; r.name = "Human"; + r.factionId = WoweeChars::Alliance; + r.maleDisplayId = 49; r.femaleDisplayId = 50; + r.startingMapId = 0; r.startingZoneAreaId = 12; // Elwynn Forest + r.defaultLanguageSpellId = 668; // Common + c.races.push_back(r); + } + { + WoweeChars::Race r; + r.raceId = 2; r.name = "Orc"; + r.factionId = WoweeChars::Horde; + r.maleDisplayId = 51; r.femaleDisplayId = 52; + r.startingMapId = 1; r.startingZoneAreaId = 215; // Mulgore-ish + r.defaultLanguageSpellId = 669; // Orcish + c.races.push_back(r); + } + auto addOutfit = [&](uint32_t classId, uint32_t raceId, + std::vector items) { + WoweeChars::Outfit o; + o.classId = classId; o.raceId = raceId; o.gender = WoweeChars::Male; + o.items = std::move(items); + c.outfits.push_back(o); + }; + // Each outfit uses WIT itemIds (1 = Worn Shortsword, + // 2 = Linen Vest, 3 = Healing Potion). + addOutfit(1, 1, {{1, 13}, {2, 5}, {3, 0}}); // Human Warrior + addOutfit(1, 2, {{1, 13}, {2, 5}, {3, 0}}); // Orc Warrior + addOutfit(8, 1, {{2, 5}, {3, 0}}); // Human Mage + addOutfit(8, 2, {{2, 5}, {3, 0}}); // Orc Mage + return c; +} + +WoweeChars WoweeCharsLoader::makeAlliance(const std::string& catalogName) { + WoweeChars c; + c.name = catalogName; + auto addClass = [&](uint32_t id, const char* name, uint8_t power, + uint32_t baseHp, uint16_t hpPerLvl, + uint32_t basePwr, uint16_t pwrPerLvl) { + WoweeChars::Class cls; + cls.classId = id; cls.name = name; + cls.powerType = power; cls.displayPower = power; + cls.baseHealth = baseHp; cls.baseHealthPerLevel = hpPerLvl; + cls.basePower = basePwr; cls.basePowerPerLevel = pwrPerLvl; + cls.factionAvailability = WoweeChars::AvailableAlliance; + c.classes.push_back(cls); + }; + addClass(1, "Warrior", WoweeChars::Rage, 60, 18, 100, 0); + addClass(2, "Paladin", WoweeChars::Mana, 55, 16, 100, 30); + addClass(4, "Rogue", WoweeChars::Energy, 45, 14, 100, 0); + addClass(8, "Mage", WoweeChars::Mana, 35, 10, 100, 30); + auto addRace = [&](uint32_t id, const char* name, uint32_t mapId, + uint32_t zoneId, uint32_t langSpell) { + WoweeChars::Race r; + r.raceId = id; r.name = name; + r.factionId = WoweeChars::Alliance; + r.startingMapId = mapId; r.startingZoneAreaId = zoneId; + r.defaultLanguageSpellId = langSpell; + c.races.push_back(r); + }; + addRace(1, "Human", 0, 12, 668); // Elwynn / Common + addRace(3, "Dwarf", 0, 1, 672); // Dun Morogh / Dwarvish + addRace(4, "NightElf", 1, 141, 671); // Teldrassil / Darnassian + addRace(7, "Gnome", 0, 1, 7340); // Dun Morogh / Gnomish + return c; +} + +WoweeChars WoweeCharsLoader::makeAllRaces(const std::string& catalogName) { + WoweeChars c; + c.name = catalogName; + auto addClass = [&](uint32_t id, const char* name, uint8_t power) { + WoweeChars::Class cls; + cls.classId = id; cls.name = name; + cls.powerType = power; cls.displayPower = power; + cls.baseHealth = 50; cls.baseHealthPerLevel = 12; + cls.basePower = 100; cls.basePowerPerLevel = 5; + c.classes.push_back(cls); + }; + addClass(1, "Warrior", WoweeChars::Rage); + addClass(2, "Paladin", WoweeChars::Mana); + addClass(3, "Hunter", WoweeChars::Mana); + addClass(4, "Rogue", WoweeChars::Energy); + addClass(5, "Priest", WoweeChars::Mana); + addClass(7, "Shaman", WoweeChars::Mana); + addClass(8, "Mage", WoweeChars::Mana); + addClass(9, "Warlock", WoweeChars::Mana); + addClass(11, "Druid", WoweeChars::Mana); + auto addRace = [&](uint32_t id, const char* name, uint8_t faction) { + WoweeChars::Race r; + r.raceId = id; r.name = name; r.factionId = faction; + c.races.push_back(r); + }; + // Alliance. + addRace(1, "Human", WoweeChars::Alliance); + addRace(3, "Dwarf", WoweeChars::Alliance); + addRace(4, "NightElf", WoweeChars::Alliance); + addRace(7, "Gnome", WoweeChars::Alliance); + // Horde. + addRace(2, "Orc", WoweeChars::Horde); + addRace(5, "Undead", WoweeChars::Horde); + addRace(6, "Tauren", WoweeChars::Horde); + addRace(8, "Troll", WoweeChars::Horde); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 8ca2cf3f..1c4a89a3 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -76,6 +76,8 @@ const char* const kArgRequired[] = { "--export-wtal-json", "--import-wtal-json", "--gen-maps", "--gen-maps-classic", "--gen-maps-bgarena", "--info-wms", "--validate-wms", + "--gen-chars", "--gen-chars-alliance", "--gen-chars-allraces", + "--info-wchc", "--validate-wchc", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_chars_catalog.cpp b/tools/editor/cli_chars_catalog.cpp new file mode 100644 index 00000000..533f4208 --- /dev/null +++ b/tools/editor/cli_chars_catalog.cpp @@ -0,0 +1,333 @@ +#include "cli_chars_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_chars.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWchcExt(std::string base) { + stripExt(base, ".wchc"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeChars& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeCharsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wchc\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeChars& c, + const std::string& base) { + std::printf("Wrote %s.wchc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" classes : %zu races : %zu outfits : %zu\n", + c.classes.size(), c.races.size(), c.outfits.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterChars"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWchcExt(base); + auto c = wowee::pipeline::WoweeCharsLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-chars")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAlliance(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AllianceChars"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWchcExt(base); + auto c = wowee::pipeline::WoweeCharsLoader::makeAlliance(name); + if (!saveOrError(c, base, "gen-chars-alliance")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAllRaces(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AllRacesChars"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWchcExt(base); + auto c = wowee::pipeline::WoweeCharsLoader::makeAllRaces(name); + if (!saveOrError(c, base, "gen-chars-allraces")) 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 = stripWchcExt(base); + if (!wowee::pipeline::WoweeCharsLoader::exists(base)) { + std::fprintf(stderr, "WCHC not found: %s.wchc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCharsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wchc"] = base + ".wchc"; + j["name"] = c.name; + j["classCount"] = c.classes.size(); + j["raceCount"] = c.races.size(); + j["outfitCount"] = c.outfits.size(); + nlohmann::json ca = nlohmann::json::array(); + for (const auto& cls : c.classes) { + ca.push_back({ + {"classId", cls.classId}, + {"name", cls.name}, + {"iconPath", cls.iconPath}, + {"powerType", cls.powerType}, + {"powerTypeName", wowee::pipeline::WoweeChars::powerTypeName(cls.powerType)}, + {"displayPower", cls.displayPower}, + {"baseHealth", cls.baseHealth}, + {"baseHealthPerLevel", cls.baseHealthPerLevel}, + {"basePower", cls.basePower}, + {"basePowerPerLevel", cls.basePowerPerLevel}, + {"factionAvailability", cls.factionAvailability}, + }); + } + j["classes"] = ca; + nlohmann::json ra = nlohmann::json::array(); + for (const auto& r : c.races) { + ra.push_back({ + {"raceId", r.raceId}, + {"name", r.name}, + {"iconPath", r.iconPath}, + {"factionId", r.factionId}, + {"factionName", wowee::pipeline::WoweeChars::raceFactionName(r.factionId)}, + {"maleDisplayId", r.maleDisplayId}, + {"femaleDisplayId", r.femaleDisplayId}, + {"baseStrength", r.baseStrength}, + {"baseAgility", r.baseAgility}, + {"baseStamina", r.baseStamina}, + {"baseIntellect", r.baseIntellect}, + {"baseSpirit", r.baseSpirit}, + {"startingMapId", r.startingMapId}, + {"startingZoneAreaId", r.startingZoneAreaId}, + {"defaultLanguageSpellId", r.defaultLanguageSpellId}, + {"mountSpellId", r.mountSpellId}, + }); + } + j["races"] = ra; + nlohmann::json oa = nlohmann::json::array(); + for (const auto& o : c.outfits) { + nlohmann::json items = nlohmann::json::array(); + for (const auto& it : o.items) { + items.push_back({ + {"itemId", it.itemId}, + {"displaySlot", it.displaySlot}, + }); + } + oa.push_back({ + {"classId", o.classId}, + {"raceId", o.raceId}, + {"gender", o.gender}, + {"items", items}, + }); + } + j["outfits"] = oa; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCHC: %s.wchc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" classes : %zu races : %zu outfits : %zu\n", + c.classes.size(), c.races.size(), c.outfits.size()); + if (!c.classes.empty()) { + std::printf("\n Classes:\n"); + std::printf(" id power baseHP /lvl name\n"); + for (const auto& cls : c.classes) { + std::printf(" %2u %-11s %4u %3u %s\n", + cls.classId, + wowee::pipeline::WoweeChars::powerTypeName(cls.powerType), + cls.baseHealth, cls.baseHealthPerLevel, + cls.name.c_str()); + } + } + if (!c.races.empty()) { + std::printf("\n Races:\n"); + std::printf(" id faction map zone name\n"); + for (const auto& r : c.races) { + std::printf(" %2u %-9s %3u %5u %s\n", + r.raceId, + wowee::pipeline::WoweeChars::raceFactionName(r.factionId), + r.startingMapId, r.startingZoneAreaId, + r.name.c_str()); + } + } + if (!c.outfits.empty()) { + std::printf("\n Outfits:\n"); + for (const auto& o : c.outfits) { + std::printf(" class=%-2u race=%-2u gender=%u items: ", + o.classId, o.raceId, o.gender); + for (size_t k = 0; k < o.items.size(); ++k) { + std::printf("%s%u@slot%u", + k > 0 ? ", " : "", + o.items[k].itemId, o.items[k].displaySlot); + } + 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 = stripWchcExt(base); + if (!wowee::pipeline::WoweeCharsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wchc: WCHC not found: %s.wchc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCharsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.classes.empty() && c.races.empty()) { + warnings.push_back("catalog has zero classes and zero races"); + } + std::vector classIdsSeen; + for (size_t k = 0; k < c.classes.size(); ++k) { + const auto& cls = c.classes[k]; + std::string ctx = "class " + std::to_string(k) + + " (id=" + std::to_string(cls.classId); + if (!cls.name.empty()) ctx += " " + cls.name; + ctx += ")"; + if (cls.classId == 0) errors.push_back(ctx + ": classId is 0"); + if (cls.name.empty()) errors.push_back(ctx + ": name is empty"); + if (cls.baseHealth == 0) { + errors.push_back(ctx + ": baseHealth is 0 (character dies on creation)"); + } + if (cls.factionAvailability == 0) { + errors.push_back(ctx + + ": factionAvailability=0 (no faction can pick this class)"); + } + for (uint32_t prev : classIdsSeen) { + if (prev == cls.classId) { + errors.push_back(ctx + ": duplicate classId"); + break; + } + } + classIdsSeen.push_back(cls.classId); + } + std::vector raceIdsSeen; + for (size_t k = 0; k < c.races.size(); ++k) { + const auto& r = c.races[k]; + std::string ctx = "race " + std::to_string(k) + + " (id=" + std::to_string(r.raceId); + if (!r.name.empty()) ctx += " " + r.name; + ctx += ")"; + if (r.raceId == 0) errors.push_back(ctx + ": raceId is 0"); + if (r.name.empty()) errors.push_back(ctx + ": name is empty"); + if (r.factionId > wowee::pipeline::WoweeChars::Neutral) { + errors.push_back(ctx + ": factionId " + + std::to_string(r.factionId) + " not in 0..2"); + } + for (uint32_t prev : raceIdsSeen) { + if (prev == r.raceId) { + errors.push_back(ctx + ": duplicate raceId"); + break; + } + } + raceIdsSeen.push_back(r.raceId); + } + // Outfit cross-references must hit real classes / races. + for (size_t k = 0; k < c.outfits.size(); ++k) { + const auto& o = c.outfits[k]; + std::string ctx = "outfit " + std::to_string(k) + + " (class=" + std::to_string(o.classId) + + " race=" + std::to_string(o.raceId) + ")"; + if (!c.classes.empty() && !c.findClass(o.classId)) { + errors.push_back(ctx + ": classId does not exist in this catalog"); + } + if (!c.races.empty() && !c.findRace(o.raceId)) { + errors.push_back(ctx + ": raceId does not exist in this catalog"); + } + if (o.gender > wowee::pipeline::WoweeChars::Female) { + errors.push_back(ctx + ": gender " + + std::to_string(o.gender) + " not 0 or 1"); + } + if (o.items.empty()) { + warnings.push_back(ctx + + ": no items (player starts naked / unarmed)"); + } + for (size_t ii = 0; ii < o.items.size(); ++ii) { + if (o.items[ii].itemId == 0) { + errors.push_back(ctx + " item " + std::to_string(ii) + + ": itemId is 0"); + } + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wchc"] = base + ".wchc"; + 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-wchc: %s.wchc\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu classes, %zu races, %zu outfits, all IDs unique\n", + c.classes.size(), c.races.size(), c.outfits.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 handleCharsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-chars") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-chars-alliance") == 0 && i + 1 < argc) { + outRc = handleGenAlliance(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-chars-allraces") == 0 && i + 1 < argc) { + outRc = handleGenAllRaces(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wchc") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wchc") == 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_chars_catalog.hpp b/tools/editor/cli_chars_catalog.hpp new file mode 100644 index 00000000..66a88212 --- /dev/null +++ b/tools/editor/cli_chars_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleCharsCatalog(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 7fd15904..cb6e4eff 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -54,6 +54,7 @@ #include "cli_taxi_catalog.hpp" #include "cli_talents_catalog.hpp" #include "cli_maps_catalog.hpp" +#include "cli_chars_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -149,6 +150,7 @@ constexpr DispatchFn kDispatchTable[] = { handleTaxiCatalog, handleTalentsCatalog, handleMapsCatalog, + handleCharsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 1fbe472e..567adc06 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1075,6 +1075,16 @@ void printUsage(const char* argv0) { std::printf(" Print WMS maps (id / type / expansion / max players) + areas (id / map / parent / level / faction / xp)\n"); std::printf(" --validate-wms [--json]\n"); std::printf(" Static checks: ids unique, areas reference real maps, parent areas exist + same map, BG/Arena needs maxPlayers\n"); + std::printf(" --gen-chars [name]\n"); + std::printf(" Emit .wchc starter: 2 classes (Warrior + Mage) + 2 races (Human + Orc) + 4 outfits with WIT cross-refs\n"); + std::printf(" --gen-chars-alliance [name]\n"); + std::printf(" Emit .wchc Alliance set: 4 classes (Warrior/Paladin/Rogue/Mage) + 4 races (Human/Dwarf/NightElf/Gnome)\n"); + std::printf(" --gen-chars-allraces [name]\n"); + std::printf(" Emit .wchc all 8 classic races (4 Alliance + 4 Horde) + 9 classes (no DK)\n"); + std::printf(" --info-wchc [--json]\n"); + std::printf(" Print WCHC classes (id / power / hp scaling) + races (faction / starting zone) + outfit item lists\n"); + std::printf(" --validate-wchc [--json]\n"); + std::printf(" Static checks: class+race ids unique, baseHealth>0, faction availability set, outfit refs resolve\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");