Fix character creation for expansion-filtered servers (Turtle WoW)

Filter races/classes in character creation screen by expansion profile
constraints, add 10s server response timeout, and reset Warden crypto
state on disconnect so reconnections use the correct session key.
This commit is contained in:
Kelsi 2026-02-14 00:57:33 -08:00
parent 886f4daf2e
commit 5b08e47941
4 changed files with 144 additions and 38 deletions

View file

@ -29,6 +29,9 @@ public:
void reset(); void reset();
void initializePreview(pipeline::AssetManager* am); void initializePreview(pipeline::AssetManager* am);
/** Set allowed races/classes from expansion profile. Empty = allow all (WotLK default). */
void setExpansionConstraints(const std::vector<uint32_t>& races, const std::vector<uint32_t>& classes);
private: private:
char nameBuffer[13] = {}; // WoW max name = 12 chars + null char nameBuffer[13] = {}; // WoW max name = 12 chars + null
int raceIndex = 0; int raceIndex = 0;
@ -47,6 +50,11 @@ private:
std::vector<game::Class> availableClasses; std::vector<game::Class> availableClasses;
void updateAvailableClasses(); void updateAvailableClasses();
// Expansion-filtered race/class lists
std::vector<game::Race> availableRaces_; // Alliance-first, then horde order
int allianceRaceCount_ = 0; // How many of availableRaces_ are alliance
std::vector<game::Class> expansionClasses_; // Allowed classes (empty = all)
std::function<void(const game::CharCreateData&)> onCreate; std::function<void(const game::CharCreateData&)> onCreate;
std::function<void()> onCancel; std::function<void()> onCancel;
@ -65,6 +73,7 @@ private:
int prevRangeGender_ = -1; int prevRangeGender_ = -1;
int prevRangeSkin_ = -1; int prevRangeSkin_ = -1;
int prevRangeHairStyle_ = -1; int prevRangeHairStyle_ = -1;
float createTimer_ = -1.0f; // >=0 while waiting for SMSG_CHAR_CREATE response
bool draggingPreview_ = false; bool draggingPreview_ = false;
float dragStartX_ = 0.0f; float dragStartX_ = 0.0f;

View file

@ -1721,6 +1721,12 @@ void Application::setupUICallbacks() {
// "Create Character" button on character screen // "Create Character" button on character screen
uiManager->getCharacterScreen().setOnCreateCharacter([this]() { uiManager->getCharacterScreen().setOnCreateCharacter([this]() {
uiManager->getCharacterCreateScreen().reset(); uiManager->getCharacterCreateScreen().reset();
// Apply expansion race/class constraints before showing the screen
if (expansionRegistry_ && expansionRegistry_->getActive()) {
auto* profile = expansionRegistry_->getActive();
uiManager->getCharacterCreateScreen().setExpansionConstraints(
profile->races, profile->classes);
}
uiManager->getCharacterCreateScreen().initializePreview(assetManager.get()); uiManager->getCharacterCreateScreen().initializePreview(assetManager.get());
setState(AppState::CHARACTER_CREATION); setState(AppState::CHARACTER_CREATION);
}); });

View file

@ -150,6 +150,12 @@ bool GameHandler::connect(const std::string& host,
wardenGateNextStatusLog_ = 2.0f; wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0; wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false; wardenCharEnumBlockedLogged_ = false;
wardenCrypto_.reset();
wardenState_ = WardenState::WAIT_MODULE_USE;
wardenModuleHash_.clear();
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
// Generate random client seed // Generate random client seed
this->clientSeed = generateClientSeed(); this->clientSeed = generateClientSeed();
@ -200,6 +206,12 @@ void GameHandler::disconnect() {
wardenGateNextStatusLog_ = 2.0f; wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0; wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false; wardenCharEnumBlockedLogged_ = false;
wardenCrypto_.reset();
wardenState_ = WardenState::WAIT_MODULE_USE;
wardenModuleHash_.clear();
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
setState(WorldState::DISCONNECTED); setState(WorldState::DISCONNECTED);
LOG_INFO("Disconnected from world server"); LOG_INFO("Disconnected from world server");
} }

View file

@ -5,11 +5,13 @@
#include "pipeline/dbc_layout.hpp" #include "pipeline/dbc_layout.hpp"
#include <imgui.h> #include <imgui.h>
#include <cstring> #include <cstring>
#include <algorithm>
namespace wowee { namespace wowee {
namespace ui { namespace ui {
static const game::Race allRaces[] = { // Full WotLK race/class lists (used as defaults when no expansion constraints set)
static const game::Race kAllRaces[] = {
// Alliance // Alliance
game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF, game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF,
game::Race::GNOME, game::Race::DRAENEI, game::Race::GNOME, game::Race::DRAENEI,
@ -17,22 +19,69 @@ static const game::Race allRaces[] = {
game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN, game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN,
game::Race::TROLL, game::Race::BLOOD_ELF, game::Race::TROLL, game::Race::BLOOD_ELF,
}; };
static constexpr int RACE_COUNT = 10; static constexpr int kAllRaceCount = 10;
static constexpr int ALLIANCE_COUNT = 5; static constexpr int kAllianceCount = 5;
static const game::Class allClasses[] = { static const game::Class kAllClasses[] = {
game::Class::WARRIOR, game::Class::PALADIN, game::Class::HUNTER, game::Class::WARRIOR, game::Class::PALADIN, game::Class::HUNTER,
game::Class::ROGUE, game::Class::PRIEST, game::Class::DEATH_KNIGHT, game::Class::ROGUE, game::Class::PRIEST, game::Class::DEATH_KNIGHT,
game::Class::SHAMAN, game::Class::MAGE, game::Class::WARLOCK, game::Class::SHAMAN, game::Class::MAGE, game::Class::WARLOCK,
game::Class::DRUID, game::Class::DRUID,
}; };
CharacterCreateScreen::CharacterCreateScreen() { CharacterCreateScreen::CharacterCreateScreen() {
reset(); reset();
} }
CharacterCreateScreen::~CharacterCreateScreen() = default; CharacterCreateScreen::~CharacterCreateScreen() = default;
void CharacterCreateScreen::setExpansionConstraints(
const std::vector<uint32_t>& races, const std::vector<uint32_t>& classes) {
// Build filtered race list: alliance first, then horde
availableRaces_.clear();
expansionClasses_.clear();
if (!races.empty()) {
// Alliance races in display order
for (auto r : std::initializer_list<game::Race>{
game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF,
game::Race::GNOME, game::Race::DRAENEI}) {
if (std::find(races.begin(), races.end(), static_cast<uint32_t>(r)) != races.end()) {
availableRaces_.push_back(r);
}
}
allianceRaceCount_ = static_cast<int>(availableRaces_.size());
// Horde races in display order
for (auto r : std::initializer_list<game::Race>{
game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN,
game::Race::TROLL, game::Race::BLOOD_ELF}) {
if (std::find(races.begin(), races.end(), static_cast<uint32_t>(r)) != races.end()) {
availableRaces_.push_back(r);
}
}
}
if (!classes.empty()) {
for (auto cls : kAllClasses) {
if (std::find(classes.begin(), classes.end(), static_cast<uint32_t>(cls)) != classes.end()) {
expansionClasses_.push_back(cls);
}
}
}
// If no constraints provided, fall back to WotLK defaults
if (availableRaces_.empty()) {
availableRaces_.assign(kAllRaces, kAllRaces + kAllRaceCount);
allianceRaceCount_ = kAllianceCount;
}
raceIndex = 0;
classIndex = 0;
updateAvailableClasses();
}
void CharacterCreateScreen::reset() { void CharacterCreateScreen::reset() {
std::memset(nameBuffer, 0, sizeof(nameBuffer)); std::memset(nameBuffer, 0, sizeof(nameBuffer));
raceIndex = 0; raceIndex = 0;
@ -50,7 +99,15 @@ void CharacterCreateScreen::reset() {
maxFacialHair = 8; maxFacialHair = 8;
statusMessage.clear(); statusMessage.clear();
statusIsError = false; statusIsError = false;
createTimer_ = -1.0f;
hairColorIds_.clear(); hairColorIds_.clear();
// Populate default races if not yet set by setExpansionConstraints
if (availableRaces_.empty()) {
availableRaces_.assign(kAllRaces, kAllRaces + kAllRaceCount);
allianceRaceCount_ = kAllianceCount;
}
updateAvailableClasses(); updateAvailableClasses();
// Reset preview tracking to force model reload on next render // Reset preview tracking to force model reload on next render
@ -81,20 +138,36 @@ void CharacterCreateScreen::update(float deltaTime) {
if (preview_) { if (preview_) {
preview_->update(deltaTime); preview_->update(deltaTime);
} }
// Timeout waiting for server response
if (createTimer_ >= 0.0f) {
createTimer_ += deltaTime;
if (createTimer_ > 10.0f) {
createTimer_ = -1.0f;
setStatus("Server did not respond. Try again.", true);
}
}
} }
void CharacterCreateScreen::setStatus(const std::string& msg, bool isError) { void CharacterCreateScreen::setStatus(const std::string& msg, bool isError) {
statusMessage = msg; statusMessage = msg;
statusIsError = isError; statusIsError = isError;
if (isError || msg.empty()) {
createTimer_ = -1.0f; // Stop waiting on error/clear
}
} }
void CharacterCreateScreen::updateAvailableClasses() { void CharacterCreateScreen::updateAvailableClasses() {
availableClasses.clear(); availableClasses.clear();
game::Race race = allRaces[raceIndex]; if (availableRaces_.empty() || raceIndex >= static_cast<int>(availableRaces_.size())) return;
for (auto cls : allClasses) { game::Race race = availableRaces_[raceIndex];
if (game::isValidRaceClassCombo(race, cls)) { for (auto cls : kAllClasses) {
availableClasses.push_back(cls); if (!game::isValidRaceClassCombo(race, cls)) continue;
// If expansion constraints set, only allow listed classes
if (!expansionClasses_.empty()) {
if (std::find(expansionClasses_.begin(), expansionClasses_.end(), cls) == expansionClasses_.end())
continue;
} }
availableClasses.push_back(cls);
} }
// Clamp class index // Clamp class index
if (classIndex >= static_cast<int>(availableClasses.size())) { if (classIndex >= static_cast<int>(availableClasses.size())) {
@ -123,7 +196,7 @@ void CharacterCreateScreen::updatePreviewIfNeeded() {
hairColorId = static_cast<uint8_t>(hairColor); hairColorId = static_cast<uint8_t>(hairColor);
} }
preview_->loadCharacter( preview_->loadCharacter(
allRaces[raceIndex], availableRaces_[raceIndex],
static_cast<game::Gender>(genderIndex), static_cast<game::Gender>(genderIndex),
static_cast<uint8_t>(skin), static_cast<uint8_t>(skin),
static_cast<uint8_t>(face), static_cast<uint8_t>(face),
@ -167,7 +240,7 @@ void CharacterCreateScreen::updateAppearanceRanges() {
auto dbc = assetManager_->loadDBC("CharSections.dbc"); auto dbc = assetManager_->loadDBC("CharSections.dbc");
if (!dbc) return; if (!dbc) return;
uint32_t targetRaceId = static_cast<uint32_t>(allRaces[raceIndex]); uint32_t targetRaceId = static_cast<uint32_t>(availableRaces_[raceIndex]);
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u; uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
@ -320,17 +393,19 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
ImGui::Spacing(); ImGui::Spacing();
// Race selection // Race selection (filtered by expansion)
int raceCount = static_cast<int>(availableRaces_.size());
ImGui::Text("Race:"); ImGui::Text("Race:");
ImGui::Spacing(); ImGui::Spacing();
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
if (allianceRaceCount_ > 0) {
ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "Alliance:"); ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "Alliance:");
ImGui::SameLine(); ImGui::SameLine();
for (int i = 0; i < ALLIANCE_COUNT; ++i) { for (int i = 0; i < allianceRaceCount_; ++i) {
if (i > 0) ImGui::SameLine(); if (i > 0) ImGui::SameLine();
bool selected = (raceIndex == i); bool selected = (raceIndex == i);
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 1.0f, 0.8f)); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 1.0f, 0.8f));
if (ImGui::SmallButton(game::getRaceName(allRaces[i]))) { if (ImGui::SmallButton(game::getRaceName(availableRaces_[i]))) {
if (raceIndex != i) { if (raceIndex != i) {
raceIndex = i; raceIndex = i;
classIndex = 0; classIndex = 0;
@ -340,13 +415,15 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
} }
if (selected) ImGui::PopStyleColor(); if (selected) ImGui::PopStyleColor();
} }
}
if (allianceRaceCount_ < raceCount) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Horde:"); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Horde:");
ImGui::SameLine(); ImGui::SameLine();
for (int i = ALLIANCE_COUNT; i < RACE_COUNT; ++i) { for (int i = allianceRaceCount_; i < raceCount; ++i) {
if (i > ALLIANCE_COUNT) ImGui::SameLine(); if (i > allianceRaceCount_) ImGui::SameLine();
bool selected = (raceIndex == i); bool selected = (raceIndex == i);
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.3f, 0.3f, 0.8f)); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.3f, 0.3f, 0.8f));
if (ImGui::SmallButton(game::getRaceName(allRaces[i]))) { if (ImGui::SmallButton(game::getRaceName(availableRaces_[i]))) {
if (raceIndex != i) { if (raceIndex != i) {
raceIndex = i; raceIndex = i;
classIndex = 0; classIndex = 0;
@ -356,6 +433,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
} }
if (selected) ImGui::PopStyleColor(); if (selected) ImGui::PopStyleColor();
} }
}
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::Spacing(); ImGui::Spacing();
@ -460,9 +538,10 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
setStatus("No valid class for this race.", true); setStatus("No valid class for this race.", true);
} else { } else {
setStatus("Creating character...", false); setStatus("Creating character...", false);
createTimer_ = 0.0f;
game::CharCreateData data; game::CharCreateData data;
data.name = name; data.name = name;
data.race = allRaces[raceIndex]; data.race = availableRaces_[raceIndex];
data.characterClass = availableClasses[classIndex]; data.characterClass = availableClasses[classIndex];
data.gender = currentGender; data.gender = currentGender;
data.useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine data.useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine