Add nonbinary gender support with pronoun system and server compatibility

Extends gender system beyond WoW's binary male/female to support nonbinary characters with proper they/them pronouns. Implements client-side gender mapping (nonbinary→male) for 3.3.5a server compatibility while preserving player identity through local config persistence. Adds pronoun placeholders ($p/$o/$s/$S) and three-option gender text parsing ($g<male>:<female>:<nonbinary>;) for inclusive quest and dialog text.
This commit is contained in:
Kelsi 2026-02-09 17:39:21 -08:00
parent 28aa88608f
commit 0071c24713
10 changed files with 421 additions and 32 deletions

View file

@ -88,50 +88,54 @@ const char* getGenderName(Gender gender) {
switch (gender) {
case Gender::MALE: return "Male";
case Gender::FEMALE: return "Female";
case Gender::NONBINARY: return "Nonbinary";
default: return "Unknown";
}
}
std::string getPlayerModelPath(Race race, Gender gender) {
// For nonbinary, default to male model (can be extended later for model selection)
bool useFemale = (gender == Gender::FEMALE);
switch (race) {
case Race::HUMAN:
return gender == Gender::FEMALE
return useFemale
? "Character\\Human\\Female\\HumanFemale.m2"
: "Character\\Human\\Male\\HumanMale.m2";
case Race::ORC:
return gender == Gender::FEMALE
return useFemale
? "Character\\Orc\\Female\\OrcFemale.m2"
: "Character\\Orc\\Male\\OrcMale.m2";
case Race::DWARF:
return gender == Gender::FEMALE
return useFemale
? "Character\\Dwarf\\Female\\DwarfFemale.m2"
: "Character\\Dwarf\\Male\\DwarfMale.m2";
case Race::NIGHT_ELF:
return gender == Gender::FEMALE
return useFemale
? "Character\\NightElf\\Female\\NightElfFemale.m2"
: "Character\\NightElf\\Male\\NightElfMale.m2";
case Race::UNDEAD:
return gender == Gender::FEMALE
return useFemale
? "Character\\Scourge\\Female\\ScourgeFemale.m2"
: "Character\\Scourge\\Male\\ScourgeMale.m2";
case Race::TAUREN:
return gender == Gender::FEMALE
return useFemale
? "Character\\Tauren\\Female\\TaurenFemale.m2"
: "Character\\Tauren\\Male\\TaurenMale.m2";
case Race::GNOME:
return gender == Gender::FEMALE
return useFemale
? "Character\\Gnome\\Female\\GnomeFemale.m2"
: "Character\\Gnome\\Male\\GnomeMale.m2";
case Race::TROLL:
return gender == Gender::FEMALE
return useFemale
? "Character\\Troll\\Female\\TrollFemale.m2"
: "Character\\Troll\\Male\\TrollMale.m2";
case Race::BLOOD_ELF:
return gender == Gender::FEMALE
return useFemale
? "Character\\BloodElf\\Female\\BloodElfFemale.m2"
: "Character\\BloodElf\\Male\\BloodElfMale.m2";
case Race::DRAENEI:
return gender == Gender::FEMALE
return useFemale
? "Character\\Draenei\\Female\\DraeneiFemale.m2"
: "Character\\Draenei\\Male\\DraeneiMale.m2";
default:

View file

@ -5924,6 +5924,7 @@ void GameHandler::saveCharacterConfig() {
}
out << "character_guid=" << playerGuid << "\n";
out << "gender=" << static_cast<int>(ch->gender) << "\n";
for (int i = 0; i < ACTION_BAR_SLOTS; i++) {
out << "action_bar_" << i << "_type=" << static_cast<int>(actionBar[i].type) << "\n";
out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n";
@ -5943,6 +5944,7 @@ void GameHandler::loadCharacterConfig() {
std::array<int, ACTION_BAR_SLOTS> types{};
std::array<uint32_t, ACTION_BAR_SLOTS> ids{};
bool hasSlots = false;
int savedGender = -1;
std::string line;
while (std::getline(in, line)) {
@ -5953,6 +5955,8 @@ void GameHandler::loadCharacterConfig() {
if (key == "character_guid") {
try { savedGuid = std::stoull(val); } catch (...) {}
} else if (key == "gender") {
try { savedGender = std::stoi(val); } catch (...) {}
} else if (key.rfind("action_bar_", 0) == 0) {
// Parse action_bar_N_type or action_bar_N_id
size_t firstUnderscore = 11; // length of "action_bar_"
@ -5980,6 +5984,17 @@ void GameHandler::loadCharacterConfig() {
return;
}
// Apply saved gender (allows nonbinary to persist even though server only stores male/female)
if (savedGender >= 0 && savedGender <= 2) {
for (auto& character : characters) {
if (character.guid == playerGuid) {
character.gender = static_cast<Gender>(savedGender);
LOG_INFO("Applied saved gender: ", getGenderName(character.gender));
break;
}
}
}
if (hasSlots) {
for (int i = 0; i < ACTION_BAR_SLOTS; i++) {
actionBar[i].type = static_cast<ActionBarSlot::Type>(types[i]);

View file

@ -257,10 +257,13 @@ const char* getAuthResultString(AuthResult result) {
network::Packet CharCreatePacket::build(const CharCreateData& data) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_CREATE));
// Convert nonbinary gender to server-compatible value (servers only support male/female)
Gender serverGender = toServerGender(data.gender);
packet.writeString(data.name); // null-terminated name
packet.writeUInt8(static_cast<uint8_t>(data.race));
packet.writeUInt8(static_cast<uint8_t>(data.characterClass));
packet.writeUInt8(static_cast<uint8_t>(data.gender));
packet.writeUInt8(static_cast<uint8_t>(serverGender));
packet.writeUInt8(data.skin);
packet.writeUInt8(data.face);
packet.writeUInt8(data.hairStyle);
@ -272,6 +275,7 @@ network::Packet CharCreatePacket::build(const CharCreateData& data) {
" race=", static_cast<int>(data.race),
" class=", static_cast<int>(data.characterClass),
" gender=", static_cast<int>(data.gender),
" (server gender=", static_cast<int>(serverGender), ")",
" skin=", static_cast<int>(data.skin),
" face=", static_cast<int>(data.face),
" hair=", static_cast<int>(data.hairStyle),