From 4a9c86b1e6d3974229a6db0305df88b7bb7b026e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Feb 2026 00:45:24 -0800 Subject: [PATCH] Harden transport updates and fix waterfall particle tint --- include/game/game_handler.hpp | 11 +++ src/game/game_handler.cpp | 60 +++++++++++----- src/game/transport_manager.cpp | 116 +++++++++++++------------------ src/pipeline/m2_loader.cpp | 8 ++- src/rendering/m2_renderer.cpp | 31 +++++---- src/rendering/water_renderer.cpp | 11 +++ 6 files changed, 137 insertions(+), 100 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index ffc0801f..34ad2d2d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace wowee::game { class TransportManager; @@ -516,10 +517,18 @@ public: void setPlayerOnTransport(uint64_t transportGuid, const glm::vec3& localOffset) { playerTransportGuid_ = transportGuid; playerTransportOffset_ = localOffset; + playerTransportStickyGuid_ = transportGuid; + playerTransportStickyTimer_ = 8.0f; + movementInfo.transportGuid = transportGuid; } void clearPlayerTransport() { + if (playerTransportGuid_ != 0) { + playerTransportStickyGuid_ = playerTransportGuid_; + playerTransportStickyTimer_ = std::max(playerTransportStickyTimer_, 1.5f); + } playerTransportGuid_ = 0; playerTransportOffset_ = glm::vec3(0.0f); + movementInfo.transportGuid = 0; } // Cooldowns @@ -1032,6 +1041,8 @@ private: std::unordered_set serverUpdatedTransportGuids_; uint64_t playerTransportGuid_ = 0; // Transport the player is riding (0 = none) glm::vec3 playerTransportOffset_ = glm::vec3(0.0f); // Player offset on transport + uint64_t playerTransportStickyGuid_ = 0; // Last transport player was on (temporary retention) + float playerTransportStickyTimer_ = 0.0f; // Seconds to keep sticky transport alive after transient clears std::unique_ptr transportManager_; // Transport movement manager std::vector knownSpells; std::unordered_map spellCooldowns; // spellId -> remaining seconds diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6278035e..fe4c2f43 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -217,6 +217,13 @@ void GameHandler::update(float deltaTime) { if (taxiStartGrace_ > 0.0f) { taxiStartGrace_ -= deltaTime; } + if (playerTransportStickyTimer_ > 0.0f) { + playerTransportStickyTimer_ -= deltaTime; + if (playerTransportStickyTimer_ <= 0.0f) { + playerTransportStickyTimer_ = 0.0f; + playerTransportStickyGuid_ = 0; + } + } // Taxi logic timing auto taxiStart = std::chrono::high_resolution_clock::now(); @@ -1985,11 +1992,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (entityManager.hasEntity(guid)) { const bool isKnownTransport = transportGuids_.count(guid) > 0; if (isKnownTransport) { - if (playerTransportGuid_ == guid) { - LOG_INFO("Keeping transport in-range while player is aboard: 0x", std::hex, guid, std::dec); - continue; - } - LOG_INFO("Processing out-of-range removal for transport: 0x", std::hex, guid, std::dec); + // Keep transports alive across out-of-range flapping. + // Boats/zeppelins are global movers and removing them here can make + // them disappear until a later movement snapshot happens to recreate them. + const bool playerAboardNow = (playerTransportGuid_ == guid); + const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f); + const bool movementSaysAboard = (movementInfo.transportGuid == guid); + LOG_INFO("Preserving transport on out-of-range: 0x", + std::hex, guid, std::dec, + " now=", playerAboardNow, + " sticky=", stickyAboard, + " movement=", movementSaysAboard); + continue; } LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec); @@ -2006,8 +2020,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { serverUpdatedTransportGuids_.erase(guid); clearTransportAttachment(guid); if (playerTransportGuid_ == guid) { - playerTransportGuid_ = 0; - playerTransportOffset_ = glm::vec3(0.0f); + clearPlayerTransport(); } entityManager.removeEntity(guid); } @@ -2051,7 +2064,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Track player-on-transport state if (block.guid == playerGuid) { if (block.onTransport) { - playerTransportGuid_ = block.transportGuid; + setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f)); // Convert transport offset from server → canonical coordinates glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); playerTransportOffset_ = core::coords::serverToCanonical(serverOffset); @@ -2063,13 +2076,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { movementInfo.z = composed.z; } LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec, - " offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")"); + " offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")"); } else { if (playerTransportGuid_ != 0) { LOG_INFO("Player left transport"); } - playerTransportGuid_ = 0; - playerTransportOffset_ = glm::vec3(0.0f); + clearPlayerTransport(); } } @@ -2657,7 +2669,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Track player-on-transport state from MOVEMENT updates if (block.onTransport) { - playerTransportGuid_ = block.transportGuid; + setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f)); // Convert transport offset from server → canonical coordinates glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); playerTransportOffset_ = core::coords::serverToCanonical(serverOffset); @@ -2679,8 +2691,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { movementInfo.z = pos.z; if (playerTransportGuid_ != 0) { LOG_INFO("Player left transport (MOVEMENT)"); - playerTransportGuid_ = 0; - playerTransportOffset_ = glm::vec3(0.0f); + clearPlayerTransport(); } } } @@ -2772,9 +2783,17 @@ void GameHandler::handleDestroyObject(network::Packet& packet) { // Remove entity if (entityManager.hasEntity(data.guid)) { if (transportGuids_.count(data.guid) > 0) { - serverUpdatedTransportGuids_.erase(data.guid); - LOG_INFO("Ignoring destroy for transport entity: 0x", std::hex, data.guid, std::dec); - return; + const bool playerAboardNow = (playerTransportGuid_ == data.guid); + const bool stickyAboard = (playerTransportStickyGuid_ == data.guid && playerTransportStickyTimer_ > 0.0f); + const bool movementSaysAboard = (movementInfo.transportGuid == data.guid); + if (playerAboardNow || stickyAboard || movementSaysAboard) { + serverUpdatedTransportGuids_.erase(data.guid); + LOG_INFO("Preserving in-use transport on destroy: 0x", std::hex, data.guid, std::dec, + " now=", playerAboardNow, + " sticky=", stickyAboard, + " movement=", movementSaysAboard); + return; + } } // Mirror out-of-range handling: invoke render-layer despawn callbacks before entity removal. auto entity = entityManager.getEntity(data.guid); @@ -2785,6 +2804,13 @@ void GameHandler::handleDestroyObject(network::Packet& packet) { gameObjectDespawnCallback_(data.guid); } } + if (transportGuids_.count(data.guid) > 0) { + transportGuids_.erase(data.guid); + serverUpdatedTransportGuids_.erase(data.guid); + if (playerTransportGuid_ == data.guid) { + clearPlayerTransport(); + } + } clearTransportAttachment(data.guid); entityManager.removeEntity(data.guid); LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec, diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 9f149760..0fcf01c3 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -57,22 +57,13 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn transport.position = spawnWorldPos; // Start at spawn position (base + offset0) - // Sanity check: firstWaypoint should match spawnWorldPos + // TransportAnimation paths are local offsets; first waypoint is expected near origin. + // Warn only if the local path itself looks suspicious. glm::vec3 firstWaypoint = path.points[0].pos; - glm::vec3 waypointDiff = spawnWorldPos - firstWaypoint; - const float mismatchDist = glm::length(waypointDiff); - if (mismatchDist > 1.0f) { + if (glm::length(firstWaypoint) > 10.0f) { LOG_WARNING("Transport 0x", std::hex, guid, std::dec, " path ", pathId, - ": firstWaypoint mismatch! spawnPos=(", spawnWorldPos.x, ",", spawnWorldPos.y, ",", spawnWorldPos.z, ")", - " firstWaypoint=(", firstWaypoint.x, ",", firstWaypoint.y, ",", firstWaypoint.z, ")", - " diff=(", waypointDiff.x, ",", waypointDiff.y, ",", waypointDiff.z, ")"); - } - const bool firstWaypointIsOrigin = glm::dot(firstWaypoint, firstWaypoint) < 0.0001f; - if (mismatchDist > 500.0f || (firstWaypointIsOrigin && mismatchDist > 50.0f)) { - transport.allowBootstrapVelocity = false; - LOG_WARNING("Transport 0x", std::hex, guid, std::dec, - " disabling DBC bootstrap velocity due to large spawn/path mismatch (dist=", - mismatchDist, ", firstIsOrigin=", firstWaypointIsOrigin, ")"); + ": first local waypoint far from origin: (", + firstWaypoint.x, ",", firstWaypoint.y, ",", firstWaypoint.z, ")"); } } @@ -576,14 +567,17 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos if (dt > 0.001f) { glm::vec3 v = (position - prevPos) / dt; const float speed = glm::length(v); + constexpr float kMinAuthoritativeSpeed = 0.15f; constexpr float kMaxSpeed = 60.0f; - if (speed > kMaxSpeed) { - v *= (kMaxSpeed / speed); - } + if (speed >= kMinAuthoritativeSpeed) { + if (speed > kMaxSpeed) { + v *= (kMaxSpeed / speed); + } - transport->serverLinearVelocity = v; - transport->serverAngularVelocity = 0.0f; - transport->hasServerVelocity = true; + transport->serverLinearVelocity = v; + transport->serverAngularVelocity = 0.0f; + transport->hasServerVelocity = true; + } } } else { // Bootstrap velocity from mapped DBC path on first authoritative sample. @@ -611,55 +605,41 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos std::sqrt(bestDistSq), ", path=", transport->pathId, ")"); } else { size_t n = path.points.size(); - size_t nextIdx = (bestIdx + 1) % n; - uint32_t t0 = path.points[bestIdx].tMs; - uint32_t t1 = path.points[nextIdx].tMs; - if (nextIdx == 0 && t1 <= t0 && path.durationMs > 0) { - t1 = path.durationMs; + constexpr float kMinBootstrapSpeed = 0.25f; + constexpr float kMaxSpeed = 60.0f; + + auto tryApplySegment = [&](size_t a, size_t b) { + uint32_t t0 = path.points[a].tMs; + uint32_t t1 = path.points[b].tMs; + if (b == 0 && t1 <= t0 && path.durationMs > 0) { + t1 = path.durationMs; + } + if (t1 <= t0) return; + glm::vec3 seg = path.points[b].pos - path.points[a].pos; + float dtSeg = static_cast(t1 - t0) / 1000.0f; + if (dtSeg <= 0.001f) return; + glm::vec3 v = seg / dtSeg; + float speed = glm::length(v); + if (speed < kMinBootstrapSpeed) return; + if (speed > kMaxSpeed) { + v *= (kMaxSpeed / speed); + } + transport->serverLinearVelocity = v; + transport->serverAngularVelocity = 0.0f; + transport->hasServerVelocity = true; + }; + + // Prefer nearest forward meaningful segment from bestIdx. + for (size_t step = 1; step < n && !transport->hasServerVelocity; ++step) { + size_t a = (bestIdx + step - 1) % n; + size_t b = (bestIdx + step) % n; + tryApplySegment(a, b); } - if (t1 <= t0 && n > 2) { - size_t prevIdx = (bestIdx + n - 1) % n; - t0 = path.points[prevIdx].tMs; - t1 = path.points[bestIdx].tMs; - glm::vec3 seg = path.points[bestIdx].pos - path.points[prevIdx].pos; - float dtSeg = static_cast(t1 - t0) / 1000.0f; - if (dtSeg > 0.001f) { - glm::vec3 v = seg / dtSeg; - float speed = glm::length(v); - constexpr float kMinBootstrapSpeed = 0.25f; - constexpr float kMaxSpeed = 60.0f; - if (speed < kMinBootstrapSpeed) { - speed = 0.0f; - } - if (speed > kMaxSpeed) { - v *= (kMaxSpeed / speed); - } - if (speed >= kMinBootstrapSpeed) { - transport->serverLinearVelocity = v; - transport->serverAngularVelocity = 0.0f; - transport->hasServerVelocity = true; - } - } - } else { - glm::vec3 seg = path.points[nextIdx].pos - path.points[bestIdx].pos; - float dtSeg = static_cast(t1 - t0) / 1000.0f; - if (dtSeg > 0.001f) { - glm::vec3 v = seg / dtSeg; - float speed = glm::length(v); - constexpr float kMinBootstrapSpeed = 0.25f; - constexpr float kMaxSpeed = 60.0f; - if (speed < kMinBootstrapSpeed) { - speed = 0.0f; - } - if (speed > kMaxSpeed) { - v *= (kMaxSpeed / speed); - } - if (speed >= kMinBootstrapSpeed) { - transport->serverLinearVelocity = v; - transport->serverAngularVelocity = 0.0f; - transport->hasServerVelocity = true; - } - } + // Fallback: nearest backward meaningful segment. + for (size_t step = 1; step < n && !transport->hasServerVelocity; ++step) { + size_t b = (bestIdx + n - step + 1) % n; + size_t a = (bestIdx + n - step) % n; + tryApplySegment(a, b); } if (transport->hasServerVelocity) { diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index 30e248e9..7e628889 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -408,13 +408,15 @@ void parseFBlock(const std::vector& data, uint32_t offset, uint32_t ofsKeys = disk.ofsKeys; if (valueType == 0) { - // Color: 3 bytes per key {r, g, b} + // Color: 3 bytes per key. + // WotLK particle FBlock color keys are stored as BGR in practice for many assets + // (notably water/waterfall emitters). Decode to RGB explicitly. if (ofsKeys + nKeys * 3 > data.size()) return; fb.vec3Values.reserve(nKeys); for (uint32_t i = 0; i < nKeys; i++) { - uint8_t r = data[ofsKeys + i * 3 + 0]; + uint8_t b = data[ofsKeys + i * 3 + 0]; uint8_t g = data[ofsKeys + i * 3 + 1]; - uint8_t b = data[ofsKeys + i * 3 + 2]; + uint8_t r = data[ofsKeys + i * 3 + 2]; fb.vec3Values.emplace_back(r / 255.0f, g / 255.0f, b / 255.0f); } } else if (valueType == 1) { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 8ec25147..b0cad4df 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1548,25 +1548,23 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: } } - // Frustum + distance cull: skip expensive bone computation for off-screen instances - // Aggressive culling for performance (double frame rate target) + // Frustum + distance cull: skip expensive bone computation for off-screen instances. + // Keep thresholds aligned with render culling so visible distant ambient actors + // (fish/seagulls/etc.) continue animating instead of freezing in idle poses. float worldRadius = model.boundRadius * instance.scale; float cullRadius = worldRadius; + if (model.disableAnimation) { + cullRadius = std::max(cullRadius, 3.0f); + } glm::vec3 toCam = instance.position - cachedCamPos_; float distSq = glm::dot(toCam, toCam); float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / 12.0f); - if (!model.disableAnimation) { - // Ultra-aggressive animation culling for 60fps target - if (worldRadius < 0.8f) { - effectiveMaxDistSq = std::min(effectiveMaxDistSq, 25.0f * 25.0f); // Ultra tight for small - } else if (worldRadius < 1.5f) { - effectiveMaxDistSq = std::min(effectiveMaxDistSq, 50.0f * 50.0f); // Very tight for medium - } else if (worldRadius < 3.0f) { - effectiveMaxDistSq = std::min(effectiveMaxDistSq, 80.0f * 80.0f); // Tight for large - } + if (model.disableAnimation) { + effectiveMaxDistSq *= 2.6f; } if (distSq > effectiveMaxDistSq) continue; - if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, cullRadius)) continue; + float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f); + if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, paddedRadius)) continue; boneWorkIndices_.push_back(idx); } @@ -2334,6 +2332,15 @@ void M2Renderer::renderM2Particles(const glm::mat4& view, const glm::mat4& proj) float alpha = interpFBlockFloat(em.particleAlpha, lifeRatio); float scale = interpFBlockFloat(em.particleScale, lifeRatio); + // Some waterfall/spray emitters become overly dark after channel-correct decoding. + // Apply a small correction only for strongly blue-dominant particle colors. + if (color.b > color.r * 1.4f && color.b > color.g * 1.15f) { + float luma = color.r * 0.2126f + color.g * 0.7152f + color.b * 0.0722f; + color = glm::mix(color, glm::vec3(luma), 0.35f); + color *= 1.35f; + color = glm::clamp(color, glm::vec3(0.28f, 0.35f, 0.45f), glm::vec3(1.0f)); + } + GLuint tex = whiteTexture; if (p.emitterIndex < static_cast(gpu.particleTextures.size())) { tex = gpu.particleTextures[p.emitterIndex]; diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index f0404bf2..fc43130a 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -506,6 +506,17 @@ void WaterRenderer::render(const Camera& camera, float time) { glm::vec4 color = getLiquidColor(surface.liquidType); float alpha = getLiquidAlpha(surface.liquidType); + // WMO liquid material IDs are not always 1:1 with terrain LiquidType.dbc semantics. + // Avoid accidental magma/slime tint (red/green waterfalls) by forcing WMO liquids + // to water-like shading unless they're explicitly ocean. + if (surface.wmoId != 0) { + const uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4); + if (basicType == 2 || basicType == 3) { + color = glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); + alpha = 0.45f; + } + } + // City/canal liquid profile: clearer water + stronger ripples/sun shimmer. // Stormwind canals typically use LiquidType 5 in this data set. bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);