mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
feat(pipeline): add WCHC (Wowee Character Classes/Races) format
Novel open replacement for Blizzard's CharClasses.dbc +
CharRaces.dbc + CharStartOutfit.dbc trio. The 27th open
format added to the editor — completes the foundational
character-creation surface.
One file holds three flat arrays:
• classes — playable classes (Warrior / Mage / etc.) with
power type (mana/rage/focus/energy/runic),
base HP+power scaling, faction availability
• races — playable races with faction (Alliance/Horde/
Neutral), starting map+zone, default language
spell, base stats, racial mount spell
• outfits — starting gear loadout per (class, race, gender)
triple, listing item IDs and display slots
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
The starter preset's outfits use real WIT itemIds (1=Worn
Shortsword, 2=Linen Vest, 3=Healing Potion) so the demo
content stack is consistent: a freshly created Human Warrior
in WCHC starts with WIT items 1/2/3, drops them on death
into a WLOT-tracked corpse loot, and can be respawned via
WSPN, etc.
Format:
• magic "WCHC", version 1, little-endian
• classes[]: classId / name / icon / powerType / display /
baseHP+perLevel / basePower+perLevel / factionAvailability
• races[]: raceId / name / icon / factionId / male+female
displayId / 5 base stats / startingMap+zone /
defaultLanguage+mount spell IDs
• outfits[]: classId+raceId+gender + items[]
(each: itemId + displaySlot)
Enums:
• PowerType (6): Mana / Rage / Focus / Energy / RunicPower / Runes
• RaceFaction (3): Alliance / Horde / Neutral
• Gender: Male / Female
• FactionAvailability bitmask: AvailableAlliance, AvailableHorde
API: WoweeCharsLoader::save / load / exists +
WoweeChars::findClass / findRace / findOutfit (by class+race+gender).
CLI added (5 flags, 585 documented total now):
--gen-chars / --gen-chars-alliance / --gen-chars-allraces
--info-wchc / --validate-wchc
Validator catches: ids unique, baseHealth=0 (instant-death
character), factionAvailability=0 (no faction can pick),
empty names, factionId out of range, outfit references to
non-existent class/race ids (cross-format resolution),
gender > 1, outfit items with itemId=0, outfit with no
items (warning — naked character).
This commit is contained in:
parent
019104536f
commit
e66601c208
8 changed files with 926 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
147
include/pipeline/wowee_chars.hpp
Normal file
147
include/pipeline/wowee_chars.hpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<OutfitItem> items;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Class> classes;
|
||||
std::vector<Race> races;
|
||||
std::vector<Outfit> 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
|
||||
418
src/pipeline/wowee_chars.cpp
Normal file
418
src/pipeline/wowee_chars.cpp
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
#include "pipeline/wowee_chars.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'C', 'H', 'C'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
|
||||
template <typename T>
|
||||
void writePOD(std::ofstream& os, const T& v) {
|
||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPOD(std::ifstream& is, T& v) {
|
||||
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
||||
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
|
||||
}
|
||||
|
||||
void writeStr(std::ofstream& os, const std::string& s) {
|
||||
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(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<uint32_t>(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<uint32_t>(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<const char*>(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<const char*>(pad3), 2);
|
||||
writePOD(os, r.startingMapId);
|
||||
writePOD(os, r.startingZoneAreaId);
|
||||
writePOD(os, r.defaultLanguageSpellId);
|
||||
writePOD(os, r.mountSpellId);
|
||||
}
|
||||
uint32_t outfitCount = static_cast<uint32_t>(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<uint8_t>(
|
||||
o.items.size() > 255 ? 255 : o.items.size());
|
||||
writePOD(os, itemCount);
|
||||
uint8_t pad2[2] = {0, 0};
|
||||
os.write(reinterpret_cast<const char*>(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<const char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<WoweeChars::OutfitItem> 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
333
tools/editor/cli_chars_catalog.cpp
Normal file
333
tools/editor/cli_chars_catalog.cpp
Normal file
|
|
@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.classes.empty() && c.races.empty()) {
|
||||
warnings.push_back("catalog has zero classes and zero races");
|
||||
}
|
||||
std::vector<uint32_t> 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<uint32_t> 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
|
||||
11
tools/editor/cli_chars_catalog.hpp
Normal file
11
tools/editor/cli_chars_catalog.hpp
Normal file
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 <wms-base> [--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 <wchc-base> [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 <wchc-base> [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 <wchc-base> [name]\n");
|
||||
std::printf(" Emit .wchc all 8 classic races (4 Alliance + 4 Horde) + 9 classes (no DK)\n");
|
||||
std::printf(" --info-wchc <wchc-base> [--json]\n");
|
||||
std::printf(" Print WCHC classes (id / power / hp scaling) + races (faction / starting zone) + outfit item lists\n");
|
||||
std::printf(" --validate-wchc <wchc-base> [--json]\n");
|
||||
std::printf(" Static checks: class+race ids unique, baseHealth>0, faction availability set, outfit refs resolve\n");
|
||||
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue