From f0aad5e97f23187cdda11c8b03341913acd844bd Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 15:18:50 -0800 Subject: [PATCH] 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. --- src/core/application.cpp | 29 +++++++++++++++++++++++++++- src/game/world_packets.cpp | 16 ++++++++++----- src/rendering/character_renderer.cpp | 28 +++++++++++++++++++++------ src/ui/game_screen.cpp | 8 ++++---- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index f047cc72..368a8a25 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -834,11 +834,38 @@ void Application::spawnPlayerCharacter() { 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) { if (tex.type == 1 && tex.filename.empty()) { tex.filename = bodySkinPath; } 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()) { if (!underwearPaths.empty()) { tex.filename = underwearPaths[0]; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 8215121e..409064db 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -714,16 +714,16 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Spline data if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED - // Skip spline data for now - complex structure 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 finalY =*/ 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(); - } else if (splineFlags & 0x00040000) { // has final angle + } else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE /*float finalAngle =*/ packet.readFloat(); } @@ -735,10 +735,16 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock /*float durationModNext =*/ packet.readFloat(); /*float verticalAccel =*/ packet.readFloat(); - /*uint32_t effectStartTime =*/ 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++) { /*float px =*/ packet.readFloat(); /*float py =*/ packet.readFloat(); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index cd34f8d6..46de569a 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -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 // 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) { uint16_t group = batch.submeshId / 100; if (group == 0) { - // Find first non-white texture in the model - for (GLuint tid : gpuModel.textureIds) { - if (tid != whiteTexture && tid != 0) { - texId = tid; - break; + // Check if this batch's texture slot is a body skin type + uint32_t texType = 0; + if (batch.textureIndex < gpuModel.data.textureLookup.size()) { + uint16_t lk = gpuModel.data.textureLookup[batch.textureIndex]; + 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; + } + } } } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 473a6a1d..e240913e 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1709,7 +1709,7 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(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); bool open = true; @@ -1760,7 +1760,7 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(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); bool open = true; @@ -1909,8 +1909,8 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing); bool open = true; if (ImGui::Begin("Vendor", &open)) {