mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Replace hardcoded WotLK protocol constants with a data-driven architecture supporting Classic 1.12.1, TBC 2.4.3, and WotLK 3.3.5a. Each expansion has JSON profiles for opcodes, update fields, and DBC layouts, plus C++ polymorphic packet parsers for binary format differences (movement flags, speed fields, transport data, spline format, char enum layout). Key components: - ExpansionRegistry: scans Data/expansions/*/expansion.json at startup - OpcodeTable: logical enum <-> wire values loaded from JSON - UpdateFieldTable: field indices loaded from JSON per expansion - DBCLayout: schema-driven DBC field lookups replacing magic numbers - PacketParsers: WotLK/TBC/Classic parsers with correct flag positions - Multi-manifest AssetManager: layered manifests with priority ordering - HDPackManager: overlay texture packs with expansion compatibility - Auth screen expansion picker replacing hardcoded version dropdown
496 lines
17 KiB
C++
496 lines
17 KiB
C++
#include "ui/character_create_screen.hpp"
|
|
#include "rendering/character_preview.hpp"
|
|
#include "game/game_handler.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "pipeline/dbc_layout.hpp"
|
|
#include <imgui.h>
|
|
#include <cstring>
|
|
|
|
namespace wowee {
|
|
namespace ui {
|
|
|
|
static const game::Race allRaces[] = {
|
|
// Alliance
|
|
game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF,
|
|
game::Race::GNOME, game::Race::DRAENEI,
|
|
// Horde
|
|
game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN,
|
|
game::Race::TROLL, game::Race::BLOOD_ELF,
|
|
};
|
|
static constexpr int RACE_COUNT = 10;
|
|
static constexpr int ALLIANCE_COUNT = 5;
|
|
|
|
static const game::Class allClasses[] = {
|
|
game::Class::WARRIOR, game::Class::PALADIN, game::Class::HUNTER,
|
|
game::Class::ROGUE, game::Class::PRIEST, game::Class::DEATH_KNIGHT,
|
|
game::Class::SHAMAN, game::Class::MAGE, game::Class::WARLOCK,
|
|
game::Class::DRUID,
|
|
};
|
|
|
|
CharacterCreateScreen::CharacterCreateScreen() {
|
|
reset();
|
|
}
|
|
|
|
CharacterCreateScreen::~CharacterCreateScreen() = default;
|
|
|
|
void CharacterCreateScreen::reset() {
|
|
std::memset(nameBuffer, 0, sizeof(nameBuffer));
|
|
raceIndex = 0;
|
|
classIndex = 0;
|
|
genderIndex = 0;
|
|
skin = 0;
|
|
face = 0;
|
|
hairStyle = 0;
|
|
hairColor = 0;
|
|
facialHair = 0;
|
|
maxSkin = 9;
|
|
maxFace = 9;
|
|
maxHairStyle = 11;
|
|
maxHairColor = 9;
|
|
maxFacialHair = 8;
|
|
statusMessage.clear();
|
|
statusIsError = false;
|
|
hairColorIds_.clear();
|
|
updateAvailableClasses();
|
|
|
|
// Reset preview tracking to force model reload on next render
|
|
prevRaceIndex_ = -1;
|
|
prevGenderIndex_ = -1;
|
|
prevSkin_ = -1;
|
|
prevFace_ = -1;
|
|
prevHairStyle_ = -1;
|
|
prevHairColor_ = -1;
|
|
prevFacialHair_ = -1;
|
|
prevRangeRace_ = -1;
|
|
prevRangeGender_ = -1;
|
|
prevRangeSkin_ = -1;
|
|
prevRangeHairStyle_ = -1;
|
|
}
|
|
|
|
void CharacterCreateScreen::initializePreview(pipeline::AssetManager* am) {
|
|
assetManager_ = am;
|
|
if (!preview_) {
|
|
preview_ = std::make_unique<rendering::CharacterPreview>();
|
|
preview_->initialize(am);
|
|
}
|
|
// Force model reload
|
|
prevRaceIndex_ = -1;
|
|
}
|
|
|
|
void CharacterCreateScreen::update(float deltaTime) {
|
|
if (preview_) {
|
|
preview_->update(deltaTime);
|
|
}
|
|
}
|
|
|
|
void CharacterCreateScreen::setStatus(const std::string& msg, bool isError) {
|
|
statusMessage = msg;
|
|
statusIsError = isError;
|
|
}
|
|
|
|
void CharacterCreateScreen::updateAvailableClasses() {
|
|
availableClasses.clear();
|
|
game::Race race = allRaces[raceIndex];
|
|
for (auto cls : allClasses) {
|
|
if (game::isValidRaceClassCombo(race, cls)) {
|
|
availableClasses.push_back(cls);
|
|
}
|
|
}
|
|
// Clamp class index
|
|
if (classIndex >= static_cast<int>(availableClasses.size())) {
|
|
classIndex = 0;
|
|
}
|
|
}
|
|
|
|
void CharacterCreateScreen::updatePreviewIfNeeded() {
|
|
if (!preview_) return;
|
|
|
|
bool changed = (raceIndex != prevRaceIndex_ ||
|
|
genderIndex != prevGenderIndex_ ||
|
|
bodyTypeIndex != prevBodyTypeIndex_ ||
|
|
skin != prevSkin_ ||
|
|
face != prevFace_ ||
|
|
hairStyle != prevHairStyle_ ||
|
|
hairColor != prevHairColor_ ||
|
|
facialHair != prevFacialHair_);
|
|
|
|
if (changed) {
|
|
bool useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine
|
|
uint8_t hairColorId = 0;
|
|
if (!hairColorIds_.empty() && hairColor >= 0 && hairColor < static_cast<int>(hairColorIds_.size())) {
|
|
hairColorId = hairColorIds_[hairColor];
|
|
} else {
|
|
hairColorId = static_cast<uint8_t>(hairColor);
|
|
}
|
|
preview_->loadCharacter(
|
|
allRaces[raceIndex],
|
|
static_cast<game::Gender>(genderIndex),
|
|
static_cast<uint8_t>(skin),
|
|
static_cast<uint8_t>(face),
|
|
static_cast<uint8_t>(hairStyle),
|
|
hairColorId,
|
|
static_cast<uint8_t>(facialHair),
|
|
useFemaleModel);
|
|
|
|
prevRaceIndex_ = raceIndex;
|
|
prevGenderIndex_ = genderIndex;
|
|
prevBodyTypeIndex_ = bodyTypeIndex;
|
|
prevSkin_ = skin;
|
|
prevFace_ = face;
|
|
prevHairStyle_ = hairStyle;
|
|
prevHairColor_ = hairColor;
|
|
prevFacialHair_ = facialHair;
|
|
}
|
|
}
|
|
|
|
void CharacterCreateScreen::updateAppearanceRanges() {
|
|
if (raceIndex == prevRangeRace_ &&
|
|
genderIndex == prevRangeGender_ &&
|
|
skin == prevRangeSkin_ &&
|
|
hairStyle == prevRangeHairStyle_) {
|
|
return;
|
|
}
|
|
|
|
prevRangeRace_ = raceIndex;
|
|
prevRangeGender_ = genderIndex;
|
|
prevRangeSkin_ = skin;
|
|
prevRangeHairStyle_ = hairStyle;
|
|
|
|
maxSkin = 9;
|
|
maxFace = 9;
|
|
maxHairStyle = 11;
|
|
maxHairColor = 9;
|
|
maxFacialHair = 8;
|
|
hairColorIds_.clear();
|
|
|
|
if (!assetManager_) return;
|
|
auto dbc = assetManager_->loadDBC("CharSections.dbc");
|
|
if (!dbc) return;
|
|
|
|
uint32_t targetRaceId = static_cast<uint32_t>(allRaces[raceIndex]);
|
|
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
|
|
|
|
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
|
int skinMax = -1;
|
|
int hairStyleMax = -1;
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
|
|
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
|
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
|
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
|
|
|
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
|
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
|
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
|
|
|
if (baseSection == 0 && variationIndex == 0) {
|
|
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
|
|
} else if (baseSection == 3) {
|
|
hairStyleMax = std::max(hairStyleMax, static_cast<int>(variationIndex));
|
|
}
|
|
}
|
|
|
|
if (skinMax >= 0) {
|
|
maxSkin = skinMax;
|
|
if (skin > maxSkin) skin = maxSkin;
|
|
}
|
|
if (hairStyleMax >= 0) {
|
|
maxHairStyle = hairStyleMax;
|
|
if (hairStyle > maxHairStyle) hairStyle = maxHairStyle;
|
|
}
|
|
|
|
int faceMax = -1;
|
|
std::vector<uint8_t> hairColorIds;
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
|
|
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
|
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
|
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
|
|
|
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
|
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
|
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
|
|
|
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
|
|
faceMax = std::max(faceMax, static_cast<int>(variationIndex));
|
|
} else if (baseSection == 3 && variationIndex == static_cast<uint32_t>(hairStyle)) {
|
|
if (colorIndex <= 255) {
|
|
hairColorIds.push_back(static_cast<uint8_t>(colorIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (faceMax >= 0) {
|
|
maxFace = faceMax;
|
|
if (face > maxFace) face = maxFace;
|
|
}
|
|
|
|
// Hair colors: use actual available DBC IDs (not "0..maxId"), since IDs may be sparse.
|
|
if (!hairColorIds.empty()) {
|
|
std::sort(hairColorIds.begin(), hairColorIds.end());
|
|
hairColorIds.erase(std::unique(hairColorIds.begin(), hairColorIds.end()), hairColorIds.end());
|
|
hairColorIds_ = std::move(hairColorIds);
|
|
maxHairColor = std::max(0, static_cast<int>(hairColorIds_.size()) - 1);
|
|
if (hairColor > maxHairColor) hairColor = maxHairColor;
|
|
if (hairColor < 0) hairColor = 0;
|
|
}
|
|
int facialMax = -1;
|
|
auto facialDbc = assetManager_->loadDBC("CharacterFacialHairStyles.dbc");
|
|
const auto* fhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
|
|
if (facialDbc) {
|
|
for (uint32_t r = 0; r < facialDbc->getRecordCount(); r++) {
|
|
uint32_t raceId = facialDbc->getUInt32(r, fhL ? (*fhL)["RaceID"] : 0);
|
|
uint32_t sexId = facialDbc->getUInt32(r, fhL ? (*fhL)["SexID"] : 1);
|
|
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
|
uint32_t variation = facialDbc->getUInt32(r, fhL ? (*fhL)["Variation"] : 2);
|
|
facialMax = std::max(facialMax, static_cast<int>(variation));
|
|
}
|
|
}
|
|
if (facialMax >= 0) {
|
|
maxFacialHair = facialMax;
|
|
} else if (targetSexId == 1) {
|
|
maxFacialHair = 0;
|
|
}
|
|
if (facialHair > maxFacialHair) {
|
|
facialHair = maxFacialHair;
|
|
}
|
|
}
|
|
|
|
void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
|
|
// Render the preview to FBO before the ImGui frame
|
|
if (preview_) {
|
|
updatePreviewIfNeeded();
|
|
preview_->render();
|
|
}
|
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
bool hasPreview = (preview_ && preview_->getTextureId() != 0);
|
|
float previewWidth = hasPreview ? 320.0f : 0.0f;
|
|
float controlsWidth = 540.0f;
|
|
float totalWidth = hasPreview ? (previewWidth + controlsWidth + 16.0f) : 600.0f;
|
|
float totalHeight = 580.0f;
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(totalWidth, totalHeight), ImGuiCond_Always);
|
|
ImGui::SetNextWindowPos(ImVec2((displaySize.x - totalWidth) * 0.5f,
|
|
(displaySize.y - totalHeight) * 0.5f),
|
|
ImGuiCond_Always);
|
|
|
|
ImGui::Begin("Create Character", nullptr,
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
|
|
|
|
if (hasPreview) {
|
|
// Left panel: 3D preview
|
|
ImGui::BeginChild("##preview_panel", ImVec2(previewWidth, -40.0f), false);
|
|
{
|
|
// Display the FBO texture (flip Y for OpenGL)
|
|
float imgW = previewWidth - 8.0f;
|
|
float imgH = imgW * (static_cast<float>(preview_->getHeight()) /
|
|
static_cast<float>(preview_->getWidth()));
|
|
if (imgH > totalHeight - 80.0f) {
|
|
imgH = totalHeight - 80.0f;
|
|
imgW = imgH * (static_cast<float>(preview_->getWidth()) /
|
|
static_cast<float>(preview_->getHeight()));
|
|
}
|
|
|
|
ImGui::Image(
|
|
static_cast<ImTextureID>(preview_->getTextureId()),
|
|
ImVec2(imgW, imgH),
|
|
ImVec2(0.0f, 1.0f), // UV top-left (flipped Y)
|
|
ImVec2(1.0f, 0.0f)); // UV bottom-right (flipped Y)
|
|
|
|
// Mouse drag rotation on the preview image
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
|
float deltaX = ImGui::GetIO().MouseDelta.x;
|
|
preview_->rotate(deltaX * 0.2f);
|
|
}
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Drag to rotate");
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::SameLine();
|
|
|
|
// Right panel: controls
|
|
ImGui::BeginChild("##controls_panel", ImVec2(0, -40.0f), false);
|
|
}
|
|
|
|
// Name input
|
|
ImGui::Text("Name:");
|
|
ImGui::SameLine(80);
|
|
ImGui::SetNextItemWidth(200);
|
|
ImGui::InputText("##name", nameBuffer, sizeof(nameBuffer));
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Race selection
|
|
ImGui::Text("Race:");
|
|
ImGui::Spacing();
|
|
ImGui::Indent(10.0f);
|
|
ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "Alliance:");
|
|
ImGui::SameLine();
|
|
for (int i = 0; i < ALLIANCE_COUNT; ++i) {
|
|
if (i > 0) ImGui::SameLine();
|
|
bool selected = (raceIndex == i);
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 1.0f, 0.8f));
|
|
if (ImGui::SmallButton(game::getRaceName(allRaces[i]))) {
|
|
if (raceIndex != i) {
|
|
raceIndex = i;
|
|
classIndex = 0;
|
|
skin = face = hairStyle = hairColor = facialHair = 0;
|
|
updateAvailableClasses();
|
|
}
|
|
}
|
|
if (selected) ImGui::PopStyleColor();
|
|
}
|
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Horde:");
|
|
ImGui::SameLine();
|
|
for (int i = ALLIANCE_COUNT; i < RACE_COUNT; ++i) {
|
|
if (i > ALLIANCE_COUNT) ImGui::SameLine();
|
|
bool selected = (raceIndex == i);
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.3f, 0.3f, 0.8f));
|
|
if (ImGui::SmallButton(game::getRaceName(allRaces[i]))) {
|
|
if (raceIndex != i) {
|
|
raceIndex = i;
|
|
classIndex = 0;
|
|
skin = face = hairStyle = hairColor = facialHair = 0;
|
|
updateAvailableClasses();
|
|
}
|
|
}
|
|
if (selected) ImGui::PopStyleColor();
|
|
}
|
|
ImGui::Unindent(10.0f);
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Class selection
|
|
ImGui::Text("Class:");
|
|
ImGui::SameLine(80);
|
|
if (!availableClasses.empty()) {
|
|
ImGui::BeginGroup();
|
|
for (int i = 0; i < static_cast<int>(availableClasses.size()); ++i) {
|
|
if (i > 0) ImGui::SameLine();
|
|
bool selected = (classIndex == i);
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 0.8f));
|
|
if (ImGui::SmallButton(game::getClassName(availableClasses[i]))) {
|
|
classIndex = i;
|
|
}
|
|
if (selected) ImGui::PopStyleColor();
|
|
}
|
|
ImGui::EndGroup();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Gender
|
|
ImGui::Text("Gender:");
|
|
ImGui::SameLine(80);
|
|
ImGui::RadioButton("Male", &genderIndex, 0);
|
|
ImGui::SameLine();
|
|
ImGui::RadioButton("Female", &genderIndex, 1);
|
|
ImGui::SameLine();
|
|
ImGui::RadioButton("Nonbinary", &genderIndex, 2);
|
|
|
|
// Body type selection for nonbinary
|
|
if (genderIndex == 2) { // Nonbinary
|
|
ImGui::Text("Body Type:");
|
|
ImGui::SameLine(80);
|
|
ImGui::RadioButton("Masculine", &bodyTypeIndex, 0);
|
|
ImGui::SameLine();
|
|
ImGui::RadioButton("Feminine", &bodyTypeIndex, 1);
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Appearance sliders
|
|
updateAppearanceRanges();
|
|
game::Gender currentGender = static_cast<game::Gender>(genderIndex);
|
|
|
|
ImGui::Text("Appearance");
|
|
ImGui::Spacing();
|
|
|
|
float sliderWidth = hasPreview ? 180.0f : 200.0f;
|
|
float labelCol = hasPreview ? 100.0f : 120.0f;
|
|
|
|
auto slider = [&](const char* label, int* val, int maxVal) {
|
|
ImGui::Text("%s", label);
|
|
ImGui::SameLine(labelCol);
|
|
ImGui::SetNextItemWidth(sliderWidth);
|
|
char id[32];
|
|
snprintf(id, sizeof(id), "##%s", label);
|
|
ImGui::SliderInt(id, val, 0, maxVal);
|
|
};
|
|
|
|
slider("Skin", &skin, maxSkin);
|
|
slider("Face", &face, maxFace);
|
|
slider("Hair Style", &hairStyle, maxHairStyle);
|
|
slider("Hair Color", &hairColor, maxHairColor);
|
|
slider("Facial Feature", &facialHair, maxFacialHair);
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Status message
|
|
if (!statusMessage.empty()) {
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
ImVec4 color = statusIsError ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
ImGui::TextColored(color, "%s", statusMessage.c_str());
|
|
}
|
|
|
|
if (hasPreview) {
|
|
ImGui::EndChild(); // controls_panel
|
|
}
|
|
|
|
// Bottom buttons (outside children)
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Create", ImVec2(150, 35))) {
|
|
std::string name(nameBuffer);
|
|
// Trim whitespace
|
|
size_t start = name.find_first_not_of(" \t\r\n");
|
|
size_t end = name.find_last_not_of(" \t\r\n");
|
|
if (start == std::string::npos) {
|
|
name.clear();
|
|
} else {
|
|
name = name.substr(start, end - start + 1);
|
|
}
|
|
if (name.empty()) {
|
|
setStatus("Please enter a character name.", true);
|
|
} else if (availableClasses.empty()) {
|
|
setStatus("No valid class for this race.", true);
|
|
} else {
|
|
setStatus("Creating character...", false);
|
|
game::CharCreateData data;
|
|
data.name = name;
|
|
data.race = allRaces[raceIndex];
|
|
data.characterClass = availableClasses[classIndex];
|
|
data.gender = currentGender;
|
|
data.useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine
|
|
data.skin = static_cast<uint8_t>(skin);
|
|
data.face = static_cast<uint8_t>(face);
|
|
data.hairStyle = static_cast<uint8_t>(hairStyle);
|
|
if (!hairColorIds_.empty() && hairColor >= 0 && hairColor < static_cast<int>(hairColorIds_.size())) {
|
|
data.hairColor = hairColorIds_[hairColor];
|
|
} else {
|
|
data.hairColor = static_cast<uint8_t>(hairColor);
|
|
}
|
|
data.facialHair = static_cast<uint8_t>(facialHair);
|
|
if (onCreate) {
|
|
onCreate(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("Back", ImVec2(150, 35))) {
|
|
if (onCancel) {
|
|
onCancel();
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace wowee
|