Fix spline parsing, hair texture, and popup window positioning

Restore unconditional verticalAccel/effectStartTime reads in spline parser
with pointCount safety cap at 256. Load player hair texture from
CharSections.dbc instead of hardcoded path, and restrict render fallback
to not apply skin composite to hair batches. Change loot/gossip/vendor
windows to re-center on each open via ImGuiCond_Appearing.
This commit is contained in:
Kelsi 2026-02-06 15:18:50 -08:00
parent aa11ffda72
commit f0aad5e97f
4 changed files with 65 additions and 16 deletions

View file

@ -834,11 +834,38 @@ void Application::spawnPlayerCharacter() {
LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures"); LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures");
} }
// Look up hair texture from CharSections.dbc section 3
std::string hairTexturePath;
if (gameHandler) {
const game::Character* activeChar = gameHandler->getActiveCharacter();
if (activeChar) {
uint8_t hairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF;
uint8_t hairColorId = (activeChar->appearanceBytes >> 24) & 0xFF;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
uint32_t section = charSectionsDbc->getUInt32(r, 3);
uint32_t variation = charSectionsDbc->getUInt32(r, 8);
uint32_t colorIdx = charSectionsDbc->getUInt32(r, 9);
if (raceId != targetRaceId || sexId != targetSexId) continue;
if (section != 3) continue;
if (variation != hairStyleId) continue;
if (colorIdx != hairColorId) continue;
hairTexturePath = charSectionsDbc->getString(r, 4);
LOG_INFO(" DBC hair texture: ", hairTexturePath,
" (style=", (int)hairStyleId, " color=", (int)hairColorId, ")");
break;
}
}
}
for (auto& tex : model.textures) { for (auto& tex : model.textures) {
if (tex.type == 1 && tex.filename.empty()) { if (tex.type == 1 && tex.filename.empty()) {
tex.filename = bodySkinPath; tex.filename = bodySkinPath;
} else if (tex.type == 6 && tex.filename.empty()) { } else if (tex.type == 6 && tex.filename.empty()) {
tex.filename = "Character\\Human\\Hair00_00.blp"; tex.filename = hairTexturePath.empty()
? "Character\\Human\\Hair00_00.blp"
: hairTexturePath;
} else if (tex.type == 8 && tex.filename.empty()) { } else if (tex.type == 8 && tex.filename.empty()) {
if (!underwearPaths.empty()) { if (!underwearPaths.empty()) {
tex.filename = underwearPaths[0]; tex.filename = underwearPaths[0];

View file

@ -714,16 +714,16 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
// Spline data // Spline data
if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED
// Skip spline data for now - complex structure
uint32_t splineFlags = packet.readUInt32(); uint32_t splineFlags = packet.readUInt32();
LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec);
if (splineFlags & 0x00010000) { // has final point if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT
/*float finalX =*/ packet.readFloat(); /*float finalX =*/ packet.readFloat();
/*float finalY =*/ packet.readFloat(); /*float finalY =*/ packet.readFloat();
/*float finalZ =*/ packet.readFloat(); /*float finalZ =*/ packet.readFloat();
} else if (splineFlags & 0x00020000) { // has final target } else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET
/*uint64_t finalTarget =*/ packet.readUInt64(); /*uint64_t finalTarget =*/ packet.readUInt64();
} else if (splineFlags & 0x00040000) { // has final angle } else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE
/*float finalAngle =*/ packet.readFloat(); /*float finalAngle =*/ packet.readFloat();
} }
@ -735,10 +735,16 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
/*float durationModNext =*/ packet.readFloat(); /*float durationModNext =*/ packet.readFloat();
/*float verticalAccel =*/ packet.readFloat(); /*float verticalAccel =*/ packet.readFloat();
/*uint32_t effectStartTime =*/ packet.readUInt32(); /*uint32_t effectStartTime =*/ packet.readUInt32();
uint32_t pointCount = packet.readUInt32(); uint32_t pointCount = packet.readUInt32();
if (pointCount > 256) {
LOG_WARNING(" Spline pointCount=", pointCount, " exceeds maximum, capping at 0 (readPos=",
packet.getReadPos(), "/", packet.getSize(), ")");
pointCount = 0;
} else {
LOG_DEBUG(" Spline pointCount=", pointCount);
}
for (uint32_t i = 0; i < pointCount; i++) { for (uint32_t i = 0; i < pointCount; i++) {
/*float px =*/ packet.readFloat(); /*float px =*/ packet.readFloat();
/*float py =*/ packet.readFloat(); /*float py =*/ packet.readFloat();

View file

@ -1272,17 +1272,33 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
} }
} }
// For body parts with white/fallback texture, use first valid texture // For body parts with white/fallback texture, use skin (type 1) texture
// This handles humanoid models where some body parts use different texture slots // This handles humanoid models where some body parts use different texture slots
// that may not be set (e.g., baked NPC textures only set slot 0) // that may not be set (e.g., baked NPC textures only set slot 0)
// Only apply to body skin slots (type 1), NOT hair (type 6) or other types
if (texId == whiteTexture) { if (texId == whiteTexture) {
uint16_t group = batch.submeshId / 100; uint16_t group = batch.submeshId / 100;
if (group == 0) { if (group == 0) {
// Find first non-white texture in the model // Check if this batch's texture slot is a body skin type
for (GLuint tid : gpuModel.textureIds) { uint32_t texType = 0;
if (tid != whiteTexture && tid != 0) { if (batch.textureIndex < gpuModel.data.textureLookup.size()) {
texId = tid; uint16_t lk = gpuModel.data.textureLookup[batch.textureIndex];
break; if (lk < gpuModel.data.textures.size()) {
texType = gpuModel.data.textures[lk].type;
}
}
// Only fall back for body skin (type 1), underwear (type 8), or cloak (type 2)
// Do NOT apply skin composite to hair (type 6) batches
if (texType != 6) {
for (size_t ti = 0; ti < gpuModel.textureIds.size(); ti++) {
if (gpuModel.textureIds[ti] != whiteTexture && gpuModel.textureIds[ti] != 0) {
// Only use type 1 (skin) textures as fallback
if (ti < gpuModel.data.textures.size() &&
(gpuModel.data.textures[ti].type == 1 || gpuModel.data.textures[ti].type == 11)) {
texId = gpuModel.textureIds[ti];
break;
}
}
} }
} }
} }

View file

@ -1709,7 +1709,7 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
auto* window = core::Application::getInstance().getWindow(); auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f; float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
bool open = true; bool open = true;
@ -1760,7 +1760,7 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
auto* window = core::Application::getInstance().getWindow(); auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f; float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
bool open = true; bool open = true;
@ -1909,8 +1909,8 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
auto* window = core::Application::getInstance().getWindow(); auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f; float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
bool open = true; bool open = true;
if (ImGui::Begin("Vendor", &open)) { if (ImGui::Begin("Vendor", &open)) {