mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +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
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue