mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-02-20 00:04:23 -05:00
Fix transport sync and stabilize WMO/tunnel grounding
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
namespace wowee::game {
|
||||
class TransportManager;
|
||||
@@ -468,6 +469,11 @@ public:
|
||||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject move callback (online mode - triggered when gameobject position updates)
|
||||
// Parameters: guid, x, y, z (canonical), orientation
|
||||
using GameObjectMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, float orientation)>;
|
||||
void setGameObjectMoveCallback(GameObjectMoveCallback cb) { gameObjectMoveCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject despawn callback (online mode - triggered when gameobject leaves view)
|
||||
using GameObjectDespawnCallback = std::function<void(uint64_t guid)>;
|
||||
void setGameObjectDespawnCallback(GameObjectDespawnCallback cb) { gameObjectDespawnCallback_ = std::move(cb); }
|
||||
@@ -504,6 +510,7 @@ public:
|
||||
|
||||
// Check if a GUID is a known transport
|
||||
bool isTransportGuid(uint64_t guid) const { return transportGuids_.count(guid) > 0; }
|
||||
bool hasServerTransportUpdate(uint64_t guid) const { return serverUpdatedTransportGuids_.count(guid) > 0; }
|
||||
glm::vec3 getComposedWorldPosition(); // Compose transport transform * local offset
|
||||
TransportManager* getTransportManager() { return transportManager_.get(); }
|
||||
void setPlayerOnTransport(uint64_t transportGuid, const glm::vec3& localOffset) {
|
||||
@@ -886,6 +893,11 @@ private:
|
||||
* Fail connection with reason
|
||||
*/
|
||||
void fail(const std::string& reason);
|
||||
void updateAttachedTransportChildren(float deltaTime);
|
||||
void setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid,
|
||||
const glm::vec3& localOffset, bool hasLocalOrientation,
|
||||
float localOrientation);
|
||||
void clearTransportAttachment(uint64_t childGuid);
|
||||
|
||||
// Network
|
||||
std::unique_ptr<network::WorldSocket> socket;
|
||||
@@ -1004,10 +1016,20 @@ private:
|
||||
TransportMoveCallback transportMoveCallback_;
|
||||
TransportSpawnCallback transportSpawnCallback_;
|
||||
GameObjectSpawnCallback gameObjectSpawnCallback_;
|
||||
GameObjectMoveCallback gameObjectMoveCallback_;
|
||||
GameObjectDespawnCallback gameObjectDespawnCallback_;
|
||||
|
||||
// Transport tracking
|
||||
struct TransportAttachment {
|
||||
ObjectType type = ObjectType::OBJECT;
|
||||
uint64_t transportGuid = 0;
|
||||
glm::vec3 localOffset{0.0f};
|
||||
float localOrientation = 0.0f;
|
||||
bool hasLocalOrientation = false;
|
||||
};
|
||||
std::unordered_map<uint64_t, TransportAttachment> transportAttachments_;
|
||||
std::unordered_set<uint64_t> transportGuids_; // GUIDs of known transport GameObjects
|
||||
std::unordered_set<uint64_t> serverUpdatedTransportGuids_;
|
||||
uint64_t playerTransportGuid_ = 0; // Transport the player is riding (0 = none)
|
||||
glm::vec3 playerTransportOffset_ = glm::vec3(0.0f); // Player offset on transport
|
||||
std::unique_ptr<TransportManager> transportManager_; // Transport movement manager
|
||||
|
||||
@@ -165,6 +165,7 @@ private:
|
||||
|
||||
// Cached isInsideWMO result (throttled to avoid per-frame cost)
|
||||
bool cachedInsideWMO = false;
|
||||
bool cachedInsideInteriorWMO = false;
|
||||
int insideWMOCheckCounter = 0;
|
||||
glm::vec3 lastInsideWMOCheckPos = glm::vec3(0.0f);
|
||||
|
||||
|
||||
@@ -430,6 +430,7 @@ private:
|
||||
// Animation update buffers (avoid per-frame allocation)
|
||||
std::vector<size_t> boneWorkIndices_; // Reused each frame
|
||||
std::vector<std::future<void>> animFutures_; // Reused each frame
|
||||
bool spatialIndexDirty_ = false;
|
||||
|
||||
// Smoke particle system
|
||||
std::vector<SmokeParticle> smokeParticles;
|
||||
|
||||
@@ -498,13 +498,15 @@ void Application::update(float deltaTime) {
|
||||
(gameHandler->isOnTaxiFlight() ||
|
||||
gameHandler->isTaxiMountActive() ||
|
||||
gameHandler->isTaxiActivationPending());
|
||||
bool onTransportNow = gameHandler && gameHandler->isOnTransport();
|
||||
if (worldEntryMovementGraceTimer_ > 0.0f) {
|
||||
worldEntryMovementGraceTimer_ -= deltaTime;
|
||||
}
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->setExternalFollow(onTaxi);
|
||||
renderer->getCameraController()->setExternalMoving(onTaxi);
|
||||
if (onTaxi) {
|
||||
const bool externallyDrivenMotion = onTaxi || onTransportNow;
|
||||
renderer->getCameraController()->setExternalFollow(externallyDrivenMotion);
|
||||
renderer->getCameraController()->setExternalMoving(externallyDrivenMotion);
|
||||
if (externallyDrivenMotion) {
|
||||
// Drop any stale local movement toggles while server drives taxi motion.
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
@@ -618,6 +620,8 @@ void Application::update(float deltaTime) {
|
||||
glm::vec3 canonical = gameHandler->getComposedWorldPosition();
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer->getCharacterPosition() = renderPos;
|
||||
// Keep movementInfo in lockstep with composed transport world position.
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
// Update camera follow target
|
||||
if (renderer->getCameraController()) {
|
||||
glm::vec3* followTarget = renderer->getCameraController()->getFollowTargetMutable();
|
||||
@@ -1117,6 +1121,30 @@ void Application::setupUICallbacks() {
|
||||
}
|
||||
});
|
||||
|
||||
gameHandler->setGameObjectMoveCallback([this](uint64_t guid, float x, float y, float z, float orientation) {
|
||||
auto it = gameObjectInstances_.find(guid);
|
||||
if (it == gameObjectInstances_.end() || !renderer) {
|
||||
return;
|
||||
}
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
auto& info = it->second;
|
||||
if (info.isWmo) {
|
||||
if (auto* wr = renderer->getWMORenderer()) {
|
||||
glm::mat4 transform(1.0f);
|
||||
transform = glm::translate(transform, renderPos);
|
||||
transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1));
|
||||
wr->setInstanceTransform(info.instanceId, transform);
|
||||
}
|
||||
} else {
|
||||
if (auto* mr = renderer->getM2Renderer()) {
|
||||
glm::mat4 transform(1.0f);
|
||||
transform = glm::translate(transform, renderPos);
|
||||
transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1));
|
||||
mr->setInstanceTransform(info.instanceId, transform);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Transport spawn callback (online mode) - register transports with TransportManager
|
||||
gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
auto* transportManager = gameHandler->getTransportManager();
|
||||
@@ -1136,10 +1164,12 @@ void Application::setupUICallbacks() {
|
||||
|
||||
// TransportAnimation.dbc is indexed by GameObject entry
|
||||
uint32_t pathId = entry;
|
||||
const bool preferServerData = gameHandler && gameHandler->hasServerTransportUpdate(guid);
|
||||
|
||||
bool clientAnim = transportManager->isClientSideAnimation();
|
||||
LOG_INFO("Transport spawn callback: clientAnimation=", clientAnim,
|
||||
" guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId);
|
||||
" guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId,
|
||||
" preferServer=", preferServerData);
|
||||
|
||||
// Coordinates are already canonical (converted in game_handler.cpp when entity was created)
|
||||
glm::vec3 canonicalSpawnPos(x, y, z);
|
||||
@@ -1156,7 +1186,25 @@ void Application::setupUICallbacks() {
|
||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
||||
}
|
||||
|
||||
if (!hasUsablePath) {
|
||||
if (preferServerData) {
|
||||
// Server-first mode: keep authoritative server snapshots, but still choose a
|
||||
// deterministic DBC path (entry/remap) as fallback if updates go stale.
|
||||
if (!hasUsablePath) {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Server-first transport registration: fallback path ", pathId,
|
||||
" for entry ", entry, " displayId=", displayId);
|
||||
} else {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_INFO("Server-first transport registration: stationary fallback for GUID 0x",
|
||||
std::hex, guid, std::dec, " entry=", entry);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Server-first transport registration: using entry DBC path for entry ", entry);
|
||||
}
|
||||
} else if (!hasUsablePath) {
|
||||
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
@@ -1220,6 +1268,7 @@ void Application::setupUICallbacks() {
|
||||
|
||||
// TransportAnimation.dbc is indexed by GameObject entry
|
||||
uint32_t pathId = entry;
|
||||
const bool preferServerData = gameHandler && gameHandler->hasServerTransportUpdate(guid);
|
||||
|
||||
// Coordinates are already canonical (converted in game_handler.cpp)
|
||||
glm::vec3 canonicalSpawnPos(x, y, z);
|
||||
@@ -1234,7 +1283,25 @@ void Application::setupUICallbacks() {
|
||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
||||
}
|
||||
|
||||
if (!hasUsablePath) {
|
||||
if (preferServerData) {
|
||||
if (!hasUsablePath) {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Auto-spawned transport in server-first mode with fallback path: entry=", entry,
|
||||
" remappedPath=", pathId, " displayId=", displayId,
|
||||
" wmoInstance=", wmoInstanceId);
|
||||
} else {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_INFO("Auto-spawned transport in server-first mode (stationary fallback): entry=", entry,
|
||||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Auto-spawned transport in server-first mode with entry DBC path: entry=", entry,
|
||||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
}
|
||||
} else if (!hasUsablePath) {
|
||||
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
@@ -3150,11 +3217,19 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||
" pos=(", x, ", ", y, ", ", z, ")");
|
||||
if (renderer) {
|
||||
if (info.isWmo) {
|
||||
if (auto* wr = renderer->getWMORenderer())
|
||||
wr->setInstancePosition(info.instanceId, renderPos);
|
||||
if (auto* wr = renderer->getWMORenderer()) {
|
||||
glm::mat4 transform(1.0f);
|
||||
transform = glm::translate(transform, renderPos);
|
||||
transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1));
|
||||
wr->setInstanceTransform(info.instanceId, transform);
|
||||
}
|
||||
} else {
|
||||
if (auto* mr = renderer->getM2Renderer())
|
||||
mr->setInstancePosition(info.instanceId, renderPos);
|
||||
if (auto* mr = renderer->getM2Renderer()) {
|
||||
glm::mat4 transform(1.0f);
|
||||
transform = glm::translate(transform, renderPos);
|
||||
transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1));
|
||||
mr->setInstanceTransform(info.instanceId, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -3170,9 +3245,9 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||
// DisplayIds 455, 462 = Elevators/Ships → try standard ship
|
||||
// DisplayIds 807, 808 = Zeppelins
|
||||
// DisplayIds 2454, 1587 = Special ships/icebreakers
|
||||
if (displayId == 455 || displayId == 462 || displayId == 20808 || displayId == 176231 || displayId == 176310) {
|
||||
if (displayId == 455 || displayId == 462 || entry == 20808 || entry == 176231 || entry == 176310) {
|
||||
modelPath = "World\\wmo\\transports\\transport_ship\\transportship.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → transportship.wmo");
|
||||
LOG_INFO("Overriding transport entry/display ", entry, "/", displayId, " → transportship.wmo");
|
||||
} else if (displayId == 807 || displayId == 808 || displayId == 175080 || displayId == 176495 || displayId == 164871) {
|
||||
modelPath = "World\\wmo\\transports\\transport_zeppelin\\transport_zeppelin.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → transport_zeppelin.wmo");
|
||||
@@ -3286,9 +3361,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||
LOG_DEBUG("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
|
||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
|
||||
// Spawn WMO doodads (chairs, furniture, etc.) as child M2 instances
|
||||
// TODO: Re-enable after implementing deferred/background loading
|
||||
// Currently disabled - spawning 134 doodads synchronously causes massive slowdown
|
||||
// Spawn transport WMO doodads (chairs, furniture, etc.) as child M2 instances
|
||||
bool isTransport = false;
|
||||
if (gameHandler) {
|
||||
std::string lowerModelPath = modelPath;
|
||||
@@ -3298,13 +3371,17 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||
}
|
||||
|
||||
auto* m2Renderer = renderer->getM2Renderer();
|
||||
if (false && m2Renderer && isTransport) { // DISABLED - too slow
|
||||
if (m2Renderer && isTransport) {
|
||||
const auto* doodadTemplates = wmoRenderer->getDoodadTemplates(modelId);
|
||||
if (doodadTemplates && !doodadTemplates->empty()) {
|
||||
LOG_INFO("Spawning ", doodadTemplates->size(), " doodads for transport WMO instance ", instanceId);
|
||||
constexpr size_t kMaxTransportDoodads = 192;
|
||||
const size_t doodadBudget = std::min(doodadTemplates->size(), kMaxTransportDoodads);
|
||||
LOG_INFO("Spawning ", doodadBudget, "/", doodadTemplates->size(),
|
||||
" doodads for transport WMO instance ", instanceId);
|
||||
int spawnedDoodads = 0;
|
||||
|
||||
for (const auto& doodadTemplate : *doodadTemplates) {
|
||||
for (size_t i = 0; i < doodadBudget; ++i) {
|
||||
const auto& doodadTemplate = (*doodadTemplates)[i];
|
||||
// Load M2 model (may be cached)
|
||||
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(doodadTemplate.m2Path));
|
||||
auto m2Data = assetManager->readFile(doodadTemplate.m2Path);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
@@ -114,6 +115,8 @@ void GameHandler::disconnect() {
|
||||
activeCharacterGuid_ = 0;
|
||||
playerNameCache.clear();
|
||||
pendingNameQueries.clear();
|
||||
transportAttachments_.clear();
|
||||
serverUpdatedTransportGuids_.clear();
|
||||
setState(WorldState::DISCONNECTED);
|
||||
LOG_INFO("Disconnected from world server");
|
||||
}
|
||||
@@ -341,6 +344,7 @@ void GameHandler::update(float deltaTime) {
|
||||
// Update transport manager
|
||||
if (transportManager_) {
|
||||
transportManager_->update(deltaTime);
|
||||
updateAttachedTransportChildren(deltaTime);
|
||||
}
|
||||
|
||||
// Distance check timing
|
||||
@@ -1829,6 +1833,14 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||
|
||||
// Add transport data if player is on a transport
|
||||
if (isOnTransport()) {
|
||||
// Keep authoritative world position synchronized to parent transport transform
|
||||
// so heartbeats/corrections don't drag the passenger through geometry.
|
||||
if (transportManager_) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
movementInfo.x = composed.x;
|
||||
movementInfo.y = composed.y;
|
||||
movementInfo.z = composed.z;
|
||||
}
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
||||
movementInfo.transportGuid = playerTransportGuid_;
|
||||
movementInfo.transportX = playerTransportOffset_.x;
|
||||
@@ -1841,8 +1853,12 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||
// ONTRANSPORT expects local orientation (player yaw relative to transport yaw).
|
||||
float transportYaw = 0.0f;
|
||||
if (transportManager_) {
|
||||
if (auto* tr = transportManager_->getTransport(playerTransportGuid_); tr && tr->hasServerYaw) {
|
||||
transportYaw = tr->serverYaw;
|
||||
if (auto* tr = transportManager_->getTransport(playerTransportGuid_); tr) {
|
||||
if (tr->hasServerYaw) {
|
||||
transportYaw = tr->serverYaw;
|
||||
} else {
|
||||
transportYaw = glm::eulerAngles(tr->rotation).z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1969,8 +1985,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
if (entityManager.hasEntity(guid)) {
|
||||
const bool isKnownTransport = transportGuids_.count(guid) > 0;
|
||||
if (isKnownTransport) {
|
||||
LOG_INFO("Ignoring out-of-range removal for transport: 0x", std::hex, guid, std::dec);
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
|
||||
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
|
||||
@@ -1984,6 +2003,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
}
|
||||
}
|
||||
transportGuids_.erase(guid);
|
||||
serverUpdatedTransportGuids_.erase(guid);
|
||||
clearTransportAttachment(guid);
|
||||
if (playerTransportGuid_ == guid) {
|
||||
playerTransportGuid_ = 0;
|
||||
playerTransportOffset_ = glm::vec3(0.0f);
|
||||
@@ -2034,6 +2055,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
// Convert transport offset from server → canonical coordinates
|
||||
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
|
||||
playerTransportOffset_ = core::coords::serverToCanonical(serverOffset);
|
||||
if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, block.orientation);
|
||||
movementInfo.x = composed.x;
|
||||
movementInfo.y = composed.y;
|
||||
movementInfo.z = composed.z;
|
||||
}
|
||||
LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec,
|
||||
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
|
||||
} else {
|
||||
@@ -2045,18 +2073,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
}
|
||||
}
|
||||
|
||||
// GameObjects with UPDATEFLAG_POSITION carry a parent transport GUID and local offset.
|
||||
// Use that to drive parent transport motion and compose correct child world position.
|
||||
if (block.objectType == ObjectType::GAMEOBJECT &&
|
||||
(block.updateFlags & 0x0100) &&
|
||||
block.onTransport &&
|
||||
block.transportGuid != 0) {
|
||||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
// Track transport-relative children so they follow parent transport motion.
|
||||
if (block.guid != playerGuid &&
|
||||
(block.objectType == ObjectType::UNIT || block.objectType == ObjectType::GAMEOBJECT)) {
|
||||
if (block.onTransport && block.transportGuid != 0) {
|
||||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING
|
||||
setTransportAttachment(block.guid, block.objectType, block.transportGuid,
|
||||
localOffset, hasLocalOrientation, block.transportO);
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
}
|
||||
} else {
|
||||
clearTransportAttachment(block.guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2200,6 +2231,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
}
|
||||
// Fire transport move callback for transports (position update on re-creation)
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
serverUpdatedTransportGuids_.insert(block.guid);
|
||||
transportMoveCallback_(block.guid,
|
||||
go->getX(), go->getY(), go->getZ(), go->getOrientation());
|
||||
}
|
||||
@@ -2360,6 +2392,28 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
// Update existing entity fields
|
||||
auto entity = entityManager.getEntity(block.guid);
|
||||
if (entity) {
|
||||
if (block.hasMovement) {
|
||||
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
|
||||
entity->setPosition(pos.x, pos.y, pos.z, block.orientation);
|
||||
|
||||
if (block.guid != playerGuid &&
|
||||
(entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::GAMEOBJECT)) {
|
||||
if (block.onTransport && block.transportGuid != 0) {
|
||||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING
|
||||
setTransportAttachment(block.guid, entity->getType(), block.transportGuid,
|
||||
localOffset, hasLocalOrientation, block.transportO);
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
}
|
||||
} else {
|
||||
clearTransportAttachment(block.guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& field : block.fields) {
|
||||
entity->setField(field.first, field.second);
|
||||
}
|
||||
@@ -2550,6 +2604,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
}
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) {
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
serverUpdatedTransportGuids_.insert(block.guid);
|
||||
transportMoveCallback_(block.guid, entity->getX(), entity->getY(),
|
||||
entity->getZ(), entity->getOrientation());
|
||||
} else if (gameObjectMoveCallback_) {
|
||||
gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(),
|
||||
entity->getZ(), entity->getOrientation());
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
||||
} else {
|
||||
@@ -2571,25 +2635,24 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
entity->setPosition(pos.x, pos.y, pos.z, block.orientation);
|
||||
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
// Some GameObject movement blocks are transport-relative: the packet carries
|
||||
// parent transport GUID + local child offset in UPDATEFLAG_POSITION.
|
||||
if (entity->getType() == ObjectType::GAMEOBJECT &&
|
||||
(block.updateFlags & 0x0100) &&
|
||||
block.onTransport &&
|
||||
block.transportGuid != 0) {
|
||||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
if (block.guid != playerGuid &&
|
||||
(entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::GAMEOBJECT)) {
|
||||
if (block.onTransport && block.transportGuid != 0) {
|
||||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING
|
||||
setTransportAttachment(block.guid, entity->getType(), block.transportGuid,
|
||||
localOffset, hasLocalOrientation, block.transportO);
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
}
|
||||
} else {
|
||||
clearTransportAttachment(block.guid);
|
||||
}
|
||||
}
|
||||
|
||||
if (block.guid == playerGuid) {
|
||||
movementInfo.x = pos.x;
|
||||
movementInfo.y = pos.y;
|
||||
movementInfo.z = pos.z;
|
||||
movementInfo.orientation = block.orientation;
|
||||
|
||||
// Track player-on-transport state from MOVEMENT updates
|
||||
@@ -2598,8 +2661,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
// Convert transport offset from server → canonical coordinates
|
||||
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
|
||||
playerTransportOffset_ = core::coords::serverToCanonical(serverOffset);
|
||||
if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, block.orientation);
|
||||
movementInfo.x = composed.x;
|
||||
movementInfo.y = composed.y;
|
||||
movementInfo.z = composed.z;
|
||||
} else {
|
||||
movementInfo.x = pos.x;
|
||||
movementInfo.y = pos.y;
|
||||
movementInfo.z = pos.z;
|
||||
}
|
||||
LOG_INFO("Player on transport (MOVEMENT): 0x", std::hex, playerTransportGuid_, std::dec);
|
||||
} else {
|
||||
movementInfo.x = pos.x;
|
||||
movementInfo.y = pos.y;
|
||||
movementInfo.z = pos.z;
|
||||
if (playerTransportGuid_ != 0) {
|
||||
LOG_INFO("Player left transport (MOVEMENT)");
|
||||
playerTransportGuid_ = 0;
|
||||
@@ -2610,8 +2687,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
|
||||
// Fire transport move callback if this is a known transport
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
serverUpdatedTransportGuids_.insert(block.guid);
|
||||
transportMoveCallback_(block.guid, pos.x, pos.y, pos.z, block.orientation);
|
||||
}
|
||||
// Fire move callback for non-transport gameobjects.
|
||||
if (entity->getType() == ObjectType::GAMEOBJECT &&
|
||||
transportGuids_.count(block.guid) == 0 &&
|
||||
gameObjectMoveCallback_) {
|
||||
gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(),
|
||||
entity->getZ(), entity->getOrientation());
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
||||
}
|
||||
@@ -2687,6 +2772,7 @@ 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;
|
||||
}
|
||||
@@ -2699,6 +2785,7 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
|
||||
gameObjectDespawnCallback_(data.guid);
|
||||
}
|
||||
}
|
||||
clearTransportAttachment(data.guid);
|
||||
entityManager.removeEntity(data.guid);
|
||||
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
|
||||
" (", (data.isDeath ? "death" : "despawn"), ")");
|
||||
@@ -4576,11 +4663,16 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) {
|
||||
if (transportManager_) {
|
||||
// Use TransportManager to compose world position from local offset
|
||||
glm::vec3 localPos(localX, localY, localZ);
|
||||
setTransportAttachment(moverGuid, entity->getType(), transportGuid, localPos, false, 0.0f);
|
||||
glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localPos);
|
||||
|
||||
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
|
||||
|
||||
LOG_INFO(" Composed NPC world position: (", worldPos.x, ", ", worldPos.y, ", ", worldPos.z, ")");
|
||||
|
||||
if (entity->getType() == ObjectType::UNIT && creatureMoveCallback_) {
|
||||
creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(" TransportManager not available for NPC position composition");
|
||||
}
|
||||
@@ -7270,6 +7362,86 @@ void GameHandler::loadCharacterConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid,
|
||||
const glm::vec3& localOffset, bool hasLocalOrientation,
|
||||
float localOrientation) {
|
||||
if (childGuid == 0 || transportGuid == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransportAttachment& attachment = transportAttachments_[childGuid];
|
||||
attachment.type = type;
|
||||
attachment.transportGuid = transportGuid;
|
||||
attachment.localOffset = localOffset;
|
||||
attachment.hasLocalOrientation = hasLocalOrientation;
|
||||
attachment.localOrientation = localOrientation;
|
||||
}
|
||||
|
||||
void GameHandler::clearTransportAttachment(uint64_t childGuid) {
|
||||
if (childGuid == 0) {
|
||||
return;
|
||||
}
|
||||
transportAttachments_.erase(childGuid);
|
||||
}
|
||||
|
||||
void GameHandler::updateAttachedTransportChildren(float /*deltaTime*/) {
|
||||
if (!transportManager_ || transportAttachments_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float kPosEpsilonSq = 0.0001f;
|
||||
constexpr float kOriEpsilon = 0.001f;
|
||||
std::vector<uint64_t> stale;
|
||||
stale.reserve(8);
|
||||
|
||||
for (const auto& [childGuid, attachment] : transportAttachments_) {
|
||||
auto entity = entityManager.getEntity(childGuid);
|
||||
if (!entity) {
|
||||
stale.push_back(childGuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
ActiveTransport* transport = transportManager_->getTransport(attachment.transportGuid);
|
||||
if (!transport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(
|
||||
attachment.transportGuid, attachment.localOffset);
|
||||
|
||||
float composedOrientation = entity->getOrientation();
|
||||
if (attachment.hasLocalOrientation) {
|
||||
float baseYaw = transport->hasServerYaw ? transport->serverYaw : 0.0f;
|
||||
composedOrientation = baseYaw + attachment.localOrientation;
|
||||
}
|
||||
|
||||
glm::vec3 oldPos(entity->getX(), entity->getY(), entity->getZ());
|
||||
float oldOrientation = entity->getOrientation();
|
||||
glm::vec3 delta = composed - oldPos;
|
||||
const bool positionChanged = glm::dot(delta, delta) > kPosEpsilonSq;
|
||||
const bool orientationChanged = std::abs(composedOrientation - oldOrientation) > kOriEpsilon;
|
||||
if (!positionChanged && !orientationChanged) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entity->setPosition(composed.x, composed.y, composed.z, composedOrientation);
|
||||
|
||||
if (attachment.type == ObjectType::UNIT) {
|
||||
if (creatureMoveCallback_) {
|
||||
creatureMoveCallback_(childGuid, composed.x, composed.y, composed.z, 0);
|
||||
}
|
||||
} else if (attachment.type == ObjectType::GAMEOBJECT) {
|
||||
if (gameObjectMoveCallback_) {
|
||||
gameObjectMoveCallback_(childGuid, composed.x, composed.y, composed.z, composedOrientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (uint64_t guid : stale) {
|
||||
transportAttachments_.erase(guid);
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 GameHandler::getComposedWorldPosition() {
|
||||
if (playerTransportGuid_ != 0 && transportManager_) {
|
||||
return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
|
||||
@@ -255,11 +255,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
||||
pathTimeMs = transport.localClockMs % path.durationMs;
|
||||
} else {
|
||||
// Server-driven transport without clock sync:
|
||||
// stay server-authoritative and never switch to DBC/client animation fallback.
|
||||
// For short server update gaps, apply lightweight dead-reckoning only when we
|
||||
// have measured velocity from at least one authoritative delta.
|
||||
constexpr float kMaxExtrapolationSec = 8.0f;
|
||||
// keep server-authoritative and dead-reckon from last known velocity.
|
||||
const float ageSec = elapsedTime_ - transport.lastServerUpdate;
|
||||
constexpr float kMaxExtrapolationSec = 30.0f;
|
||||
|
||||
if (transport.hasServerVelocity && ageSec > 0.0f && ageSec <= kMaxExtrapolationSec) {
|
||||
const float blend = glm::clamp(1.0f - (ageSec / kMaxExtrapolationSec), 0.0f, 1.0f);
|
||||
transport.position += transport.serverLinearVelocity * (deltaTime * blend);
|
||||
@@ -577,6 +576,74 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
||||
transport->serverAngularVelocity = 0.0f;
|
||||
transport->hasServerVelocity = true;
|
||||
}
|
||||
} else {
|
||||
// Bootstrap velocity from mapped DBC path on first authoritative sample.
|
||||
// This avoids "stalled at dock" when server sends sparse transport snapshots.
|
||||
auto pathIt2 = paths_.find(transport->pathId);
|
||||
if (pathIt2 != paths_.end()) {
|
||||
const auto& path = pathIt2->second;
|
||||
if (path.points.size() >= 2 && path.durationMs > 0) {
|
||||
glm::vec3 local = position - transport->basePosition;
|
||||
size_t bestIdx = 0;
|
||||
float bestDistSq = std::numeric_limits<float>::max();
|
||||
for (size_t i = 0; i < path.points.size(); ++i) {
|
||||
glm::vec3 d = path.points[i].pos - local;
|
||||
float distSq = glm::dot(d, d);
|
||||
if (distSq < bestDistSq) {
|
||||
bestDistSq = distSq;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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<float>(t1 - t0) / 1000.0f;
|
||||
if (dtSeg > 0.001f) {
|
||||
glm::vec3 v = seg / dtSeg;
|
||||
float speed = glm::length(v);
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
if (speed > kMaxSpeed) {
|
||||
v *= (kMaxSpeed / speed);
|
||||
}
|
||||
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<float>(t1 - t0) / 1000.0f;
|
||||
if (dtSeg > 0.001f) {
|
||||
glm::vec3 v = seg / dtSeg;
|
||||
float speed = glm::length(v);
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
if (speed > kMaxSpeed) {
|
||||
v *= (kMaxSpeed / speed);
|
||||
}
|
||||
transport->serverLinearVelocity = v;
|
||||
transport->serverAngularVelocity = 0.0f;
|
||||
transport->hasServerVelocity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (transport->hasServerVelocity) {
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" bootstrapped velocity from DBC path ", transport->pathId,
|
||||
" v=(", transport->serverLinearVelocity.x, ", ",
|
||||
transport->serverLinearVelocity.y, ", ",
|
||||
transport->serverLinearVelocity.z, ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTransformMatrices(*transport);
|
||||
|
||||
@@ -50,6 +50,19 @@ std::optional<float> selectHighestFloor(const std::optional<float>& a,
|
||||
return best;
|
||||
}
|
||||
|
||||
std::optional<float> selectClosestFloor(const std::optional<float>& a,
|
||||
const std::optional<float>& b,
|
||||
float refZ) {
|
||||
if (a && b) {
|
||||
float da = std::abs(*a - refZ);
|
||||
float db = std::abs(*b - refZ);
|
||||
return (da <= db) ? a : b;
|
||||
}
|
||||
if (a) return a;
|
||||
if (b) return b;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CameraController::CameraController(Camera* cam) : camera(cam) {
|
||||
@@ -537,6 +550,20 @@ void CameraController::update(float deltaTime) {
|
||||
verticalVelocity = 0.0f;
|
||||
}
|
||||
|
||||
// Refresh inside-WMO state before collision/grounding so we don't use stale
|
||||
// terrain-first caches while entering enclosed tunnel/building spaces.
|
||||
if (wmoRenderer && !externalFollow_) {
|
||||
bool prevInside = cachedInsideWMO;
|
||||
bool prevInsideInterior = cachedInsideInteriorWMO;
|
||||
cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr);
|
||||
cachedInsideInteriorWMO = wmoRenderer->isInsideInteriorWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f);
|
||||
if (cachedInsideWMO != prevInside || cachedInsideInteriorWMO != prevInsideInterior) {
|
||||
hasCachedFloor_ = false;
|
||||
hasCachedCamFloor = false;
|
||||
cachedPivotLift_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Sweep collisions in small steps to reduce tunneling through thin walls/floors.
|
||||
// Skip entirely when stationary to avoid wasting collision calls.
|
||||
// Use tighter steps when inside WMO for more precise collision.
|
||||
@@ -587,15 +614,27 @@ void CameraController::update(float deltaTime) {
|
||||
// 1. Center-only sample for terrain/WMO floor selection.
|
||||
// Using only the center prevents tunnel entrances from snapping
|
||||
// to terrain when offset samples miss the WMO floor geometry.
|
||||
// Slope limit: reject surfaces too steep to walk (prevent clipping)
|
||||
constexpr float MIN_WALKABLE_NORMAL = 0.7f; // ~45° max slope
|
||||
// Slope limit: reject surfaces too steep to walk (prevent clipping).
|
||||
// WMO tunnel/bridge ramps are often steeper than outdoor terrain ramps.
|
||||
constexpr float MIN_WALKABLE_NORMAL_TERRAIN = 0.7f; // ~45°
|
||||
constexpr float MIN_WALKABLE_NORMAL_WMO = 0.45f; // allow tunnel ramps
|
||||
|
||||
std::optional<float> groundH;
|
||||
std::optional<float> centerTerrainH;
|
||||
std::optional<float> centerWmoH;
|
||||
{
|
||||
// Collision cache: skip expensive checks if barely moved (15cm threshold)
|
||||
float distMoved = glm::length(glm::vec2(targetPos.x, targetPos.y) -
|
||||
glm::vec2(lastCollisionCheckPos_.x, lastCollisionCheckPos_.y));
|
||||
bool useCached = hasCachedFloor_ && distMoved < COLLISION_CACHE_DISTANCE;
|
||||
if (useCached) {
|
||||
// Never trust cached ground while actively descending or when
|
||||
// vertical drift from cached floor is meaningful.
|
||||
float dzCached = std::abs(targetPos.z - cachedFloorHeight_);
|
||||
if (verticalVelocity < -0.4f || dzCached > 0.35f) {
|
||||
useCached = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (useCached) {
|
||||
groundH = cachedFloorHeight_;
|
||||
@@ -613,11 +652,52 @@ void CameraController::update(float deltaTime) {
|
||||
}
|
||||
|
||||
// Reject steep WMO slopes
|
||||
if (wmoH && wmoNormalZ < MIN_WALKABLE_NORMAL) {
|
||||
float minWalkableWmo = cachedInsideWMO ? MIN_WALKABLE_NORMAL_WMO : MIN_WALKABLE_NORMAL_TERRAIN;
|
||||
if (wmoH && wmoNormalZ < minWalkableWmo) {
|
||||
wmoH = std::nullopt; // Treat as unwalkable
|
||||
}
|
||||
centerTerrainH = terrainH;
|
||||
centerWmoH = wmoH;
|
||||
|
||||
groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
|
||||
// Guard against extremely bad WMO void ramps, but keep normal tunnel
|
||||
// transitions valid. Only reject when the WMO sample is implausibly far
|
||||
// below terrain and player is not already descending.
|
||||
if (terrainH && wmoH) {
|
||||
float terrainMinusWmo = *terrainH - *wmoH;
|
||||
if (terrainMinusWmo > 12.0f && verticalVelocity > -8.0f) {
|
||||
wmoH = std::nullopt;
|
||||
centerWmoH = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedInsideWMO && wmoH) {
|
||||
// Transition seam (e.g. tunnel mouths): if terrain is much higher than
|
||||
// nearby WMO walkable floor, prefer the WMO floor so we can enter.
|
||||
bool preferWmoAtSeam = false;
|
||||
if (terrainH) {
|
||||
float terrainAboveWmo = *terrainH - *wmoH;
|
||||
float wmoDropFromPlayer = targetPos.z - *wmoH;
|
||||
float playerVsTerrain = targetPos.z - *terrainH;
|
||||
bool descendingIntoTunnel = (verticalVelocity < -1.0f) || (playerVsTerrain < -0.35f);
|
||||
if (terrainAboveWmo > 1.2f && terrainAboveWmo < 8.0f &&
|
||||
wmoDropFromPlayer >= -0.4f && wmoDropFromPlayer < 1.8f &&
|
||||
*wmoH <= targetPos.z + stepUpBudget &&
|
||||
descendingIntoTunnel) {
|
||||
preferWmoAtSeam = true;
|
||||
}
|
||||
}
|
||||
if (preferWmoAtSeam) {
|
||||
groundH = wmoH;
|
||||
} else if (terrainH) {
|
||||
// At tunnel seams where both exist, pick the one closest to current feet Z
|
||||
// to avoid oscillating between top terrain and deep WMO floors.
|
||||
groundH = selectClosestFloor(terrainH, wmoH, targetPos.z);
|
||||
} else {
|
||||
groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
|
||||
}
|
||||
} else {
|
||||
groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
lastCollisionCheckPos_ = targetPos;
|
||||
@@ -630,6 +710,81 @@ void CameraController::update(float deltaTime) {
|
||||
}
|
||||
}
|
||||
|
||||
// Transition safety: if no reachable floor was selected, choose the higher
|
||||
// of terrain/WMO center surfaces when it is still near the player.
|
||||
// This avoids dropping into void gaps at terrain<->WMO seams.
|
||||
if (!groundH) {
|
||||
auto highestCenter = selectHighestFloor(centerTerrainH, centerWmoH, std::nullopt);
|
||||
if (highestCenter) {
|
||||
float dz = targetPos.z - *highestCenter;
|
||||
// Keep this fallback narrow: only for WMO seam cases, or very short
|
||||
// transient misses while still almost touching the last floor.
|
||||
bool allowFallback = cachedInsideWMO || (noGroundTimer_ < 0.10f && dz < 0.6f);
|
||||
if (allowFallback && dz >= -0.5f && dz < 2.0f) {
|
||||
groundH = highestCenter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continuity guard only for WMO seam overlap: avoid instantly switching to a
|
||||
// much lower floor sample at tunnel mouths (bad WMO ramp chains into void).
|
||||
if (groundH && hasRealGround_ && cachedInsideWMO && !cachedInsideInteriorWMO) {
|
||||
float dropFromLast = lastGroundZ - *groundH;
|
||||
if (dropFromLast > 1.5f) {
|
||||
if (centerTerrainH && *centerTerrainH > *groundH + 1.5f) {
|
||||
groundH = centerTerrainH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seam stability: while overlapping WMO shells, cap how fast floor height can
|
||||
// step downward in a single frame to avoid following bad ramp samples into void.
|
||||
if (groundH && cachedInsideWMO && !cachedInsideInteriorWMO && lastGroundZ > 1.0f) {
|
||||
float maxDropPerFrame = (verticalVelocity < -8.0f) ? 2.0f : 0.60f;
|
||||
float minAllowed = lastGroundZ - maxDropPerFrame;
|
||||
// Extra seam guard: outside interior groups, avoid accepting floors that
|
||||
// are far below nearby terrain. Keeps shark-mouth transitions from
|
||||
// following erroneous WMO ramps into void.
|
||||
if (centerTerrainH) {
|
||||
// Never let terrain-based seam guard push floor above current feet;
|
||||
// it should only prevent excessive downward drops.
|
||||
float terrainGuard = std::min(*centerTerrainH - 1.0f, targetPos.z - 0.15f);
|
||||
minAllowed = std::max(minAllowed, terrainGuard);
|
||||
}
|
||||
if (*groundH < minAllowed) {
|
||||
*groundH = minAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 1b. Multi-sample WMO floors when in/near WMO space to avoid
|
||||
// falling through narrow board/plank gaps where center ray misses.
|
||||
if (wmoRenderer && cachedInsideWMO) {
|
||||
constexpr float WMO_FOOTPRINT = 0.35f;
|
||||
const glm::vec2 wmoOffsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
{ WMO_FOOTPRINT, 0.0f}, {-WMO_FOOTPRINT, 0.0f},
|
||||
{0.0f, WMO_FOOTPRINT}, {0.0f, -WMO_FOOTPRINT}
|
||||
};
|
||||
|
||||
float wmoProbeZ = std::max(targetPos.z, lastGroundZ) + stepUpBudget + 0.6f;
|
||||
float minWalkableWmo = cachedInsideWMO ? MIN_WALKABLE_NORMAL_WMO : MIN_WALKABLE_NORMAL_TERRAIN;
|
||||
|
||||
for (const auto& o : wmoOffsets) {
|
||||
float nz = 1.0f;
|
||||
auto wh = wmoRenderer->getFloorHeight(targetPos.x + o.x, targetPos.y + o.y, wmoProbeZ, &nz);
|
||||
if (!wh) continue;
|
||||
if (nz < minWalkableWmo) continue;
|
||||
|
||||
// Keep to nearby, walkable steps only.
|
||||
if (*wh > targetPos.z + stepUpBudget) continue;
|
||||
if (*wh < targetPos.z - 2.5f) continue;
|
||||
|
||||
if (!groundH || *wh > *groundH) {
|
||||
groundH = wh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Multi-sample for M2 objects (rugs, planks, bridges, ships) —
|
||||
// these are narrow and need offset probes to detect reliably.
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
@@ -646,7 +801,7 @@ void CameraController::update(float deltaTime) {
|
||||
targetPos.x + o.x, targetPos.y + o.y, m2ProbeZ, &m2NormalZ);
|
||||
|
||||
// Reject steep M2 slopes
|
||||
if (m2H && m2NormalZ < MIN_WALKABLE_NORMAL) {
|
||||
if (m2H && m2NormalZ < MIN_WALKABLE_NORMAL_TERRAIN) {
|
||||
continue; // Skip unwalkable M2 surface
|
||||
}
|
||||
|
||||
@@ -676,7 +831,8 @@ void CameraController::update(float deltaTime) {
|
||||
// 3. Was grounded + ground is close (grace for slopes)
|
||||
bool nearGround = (dz >= 0.0f && dz <= stepUp);
|
||||
bool airFalling = (!grounded && verticalVelocity < -5.0f);
|
||||
bool slopeGrace = (grounded && dz >= -1.0f && dz <= stepUp * 2.0f);
|
||||
bool slopeGrace = (grounded && verticalVelocity > -1.0f &&
|
||||
dz >= -0.25f && dz <= stepUp * 1.5f);
|
||||
|
||||
if (dz >= -fallCatch && (nearGround || airFalling || slopeGrace)) {
|
||||
targetPos.z = *groundH;
|
||||
@@ -691,18 +847,13 @@ void CameraController::update(float deltaTime) {
|
||||
hasRealGround_ = false;
|
||||
noGroundTimer_ += deltaTime;
|
||||
|
||||
if (noGroundTimer_ < NO_GROUND_GRACE) {
|
||||
// Grace should prevent instant falling at seams,
|
||||
// but should NEVER pull you down or cancel a jump.
|
||||
targetPos.z = std::max(targetPos.z, lastGroundZ);
|
||||
|
||||
// Only zero velocity if we're not moving upward.
|
||||
if (verticalVelocity <= 0.0f) {
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
} else {
|
||||
grounded = false; // jumping upward: don't "stick" to ghost ground
|
||||
}
|
||||
float dropFromLastGround = lastGroundZ - targetPos.z;
|
||||
bool seamSizedGap = dropFromLastGround <= 0.35f;
|
||||
if (noGroundTimer_ < NO_GROUND_GRACE && seamSizedGap) {
|
||||
// Micro-gap grace only: keep continuity for tiny seam misses,
|
||||
// but never convert air into persistent ground.
|
||||
targetPos.z = std::max(targetPos.z, lastGroundZ - 0.10f);
|
||||
grounded = false;
|
||||
} else {
|
||||
grounded = false;
|
||||
}
|
||||
@@ -738,7 +889,7 @@ void CameraController::update(float deltaTime) {
|
||||
// Pivot point at upper chest/neck.
|
||||
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f;
|
||||
float pivotLift = 0.0f;
|
||||
if (terrainManager && !externalFollow_) {
|
||||
if (terrainManager && !externalFollow_ && !cachedInsideInteriorWMO) {
|
||||
float moved = glm::length(glm::vec2(targetPos.x - lastPivotLiftQueryPos_.x,
|
||||
targetPos.y - lastPivotLiftQueryPos_.y));
|
||||
float distDelta = std::abs(currentDistance - lastPivotLiftDistance_);
|
||||
@@ -772,6 +923,10 @@ void CameraController::update(float deltaTime) {
|
||||
cachedPivotLift_ = desiredLift;
|
||||
}
|
||||
pivotLift = cachedPivotLift_;
|
||||
} else if (cachedInsideInteriorWMO) {
|
||||
// Inside WMO volumes (including tunnel/cave shells): terrain-above samples
|
||||
// are not relevant for camera pivoting.
|
||||
cachedPivotLift_ = 0.0f;
|
||||
}
|
||||
glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset + pivotLift);
|
||||
|
||||
@@ -787,26 +942,13 @@ void CameraController::update(float deltaTime) {
|
||||
if (wmoRenderer) {
|
||||
float distFromLastCheck = glm::length(targetPos - lastInsideWMOCheckPos);
|
||||
if (++insideWMOCheckCounter >= 10 || distFromLastCheck > 2.0f) {
|
||||
cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr);
|
||||
wmoRenderer->updateActiveGroup(targetPos.x, targetPos.y, targetPos.z + 1.0f);
|
||||
insideWMOCheckCounter = 0;
|
||||
lastInsideWMOCheckPos = targetPos;
|
||||
}
|
||||
|
||||
// Constrain zoom if there's a ceiling/upper floor above player
|
||||
// Raycast upward from player to find ceiling, limit camera distance
|
||||
glm::vec3 upRayOrigin = targetPos;
|
||||
glm::vec3 upRayDir(0.0f, 0.0f, 1.0f);
|
||||
float ceilingDist = wmoRenderer->raycastBoundingBoxes(upRayOrigin, upRayDir, 20.0f);
|
||||
if (ceilingDist < 20.0f) {
|
||||
// Found ceiling above — limit zoom to prevent camera from going through it
|
||||
// Camera is behind player by currentDistance, at an angle
|
||||
// Approximate: if ceiling is N units above, limit zoom to ~N units
|
||||
float maxZoomForCeiling = std::max(1.5f, ceilingDist * 0.7f);
|
||||
if (currentDistance > maxZoomForCeiling) {
|
||||
currentDistance = maxZoomForCeiling;
|
||||
}
|
||||
}
|
||||
// Do not clamp zoom target by ceiling checks. First-person should always
|
||||
// be reachable; occlusion handling below will resolve camera placement safely.
|
||||
}
|
||||
|
||||
// ===== Camera collision (sphere sweep approximation) =====
|
||||
@@ -866,8 +1008,11 @@ void CameraController::update(float deltaTime) {
|
||||
// ===== Final floor clearance check =====
|
||||
// Use WMO-aware floor so the camera doesn't pop above tunnels/caves.
|
||||
constexpr float MIN_FLOOR_CLEARANCE = 0.35f;
|
||||
{
|
||||
auto camTerrainH = getTerrainFloorAt(smoothedCamPos.x, smoothedCamPos.y);
|
||||
if (!cachedInsideWMO) {
|
||||
std::optional<float> camTerrainH;
|
||||
if (!cachedInsideInteriorWMO) {
|
||||
camTerrainH = getTerrainFloorAt(smoothedCamPos.x, smoothedCamPos.y);
|
||||
}
|
||||
std::optional<float> camWmoH;
|
||||
if (wmoRenderer) {
|
||||
// Skip expensive WMO floor query if camera barely moved
|
||||
@@ -876,15 +1021,45 @@ void CameraController::update(float deltaTime) {
|
||||
if (camDelta < 0.3f && hasCachedCamFloor) {
|
||||
camWmoH = cachedCamWmoFloor;
|
||||
} else {
|
||||
float camFloorProbeZ = smoothedCamPos.z;
|
||||
if (cachedInsideInteriorWMO) {
|
||||
// Inside tunnels/buildings, probe near player height so roof
|
||||
// triangles above the camera don't get treated as floor.
|
||||
camFloorProbeZ = std::min(smoothedCamPos.z, targetPos.z + 1.0f);
|
||||
}
|
||||
camWmoH = wmoRenderer->getFloorHeight(
|
||||
smoothedCamPos.x, smoothedCamPos.y, smoothedCamPos.z);
|
||||
smoothedCamPos.x, smoothedCamPos.y, camFloorProbeZ);
|
||||
|
||||
if (cachedInsideInteriorWMO && camWmoH) {
|
||||
// Never let camera floor clamp latch to tunnel ceilings / upper decks.
|
||||
float maxValidIndoorFloor = targetPos.z + 0.9f;
|
||||
if (*camWmoH > maxValidIndoorFloor) {
|
||||
camWmoH = std::nullopt;
|
||||
}
|
||||
}
|
||||
cachedCamWmoFloor = camWmoH;
|
||||
hasCachedCamFloor = true;
|
||||
lastCamFloorQueryPos = smoothedCamPos;
|
||||
}
|
||||
}
|
||||
auto camFloorH = selectReachableFloor(
|
||||
camTerrainH, camWmoH, smoothedCamPos.z, 0.5f);
|
||||
// When camera/character are inside a WMO, force WMO floor usage for camera
|
||||
// clearance to avoid snapping toward terrain above enclosed tunnels/caves.
|
||||
std::optional<float> camFloorH;
|
||||
if (cachedInsideWMO && camWmoH && camTerrainH) {
|
||||
// Transition seam: avoid terrain-above clamp near tunnel entrances.
|
||||
float camDropFromPlayer = targetPos.z - *camWmoH;
|
||||
if ((*camTerrainH - *camWmoH) > 1.2f &&
|
||||
(*camTerrainH - *camWmoH) < 8.0f &&
|
||||
camDropFromPlayer >= -0.4f &&
|
||||
camDropFromPlayer < 1.8f) {
|
||||
camFloorH = camWmoH;
|
||||
} else {
|
||||
camFloorH = selectClosestFloor(camTerrainH, camWmoH, smoothedCamPos.z);
|
||||
}
|
||||
} else {
|
||||
camFloorH = selectReachableFloor(
|
||||
camTerrainH, camWmoH, smoothedCamPos.z, 0.5f);
|
||||
}
|
||||
if (camFloorH && smoothedCamPos.z < *camFloorH + MIN_FLOOR_CLEARANCE) {
|
||||
smoothedCamPos.z = *camFloorH + MIN_FLOOR_CLEARANCE;
|
||||
}
|
||||
|
||||
@@ -1405,6 +1405,10 @@ static void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) {
|
||||
}
|
||||
|
||||
void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::mat4& viewProjection) {
|
||||
if (spatialIndexDirty_) {
|
||||
rebuildSpatialIndex();
|
||||
}
|
||||
|
||||
float dtMs = deltaTime * 1000.0f;
|
||||
|
||||
// Cache camera state for frustum-culling bone computation
|
||||
@@ -2485,7 +2489,7 @@ void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& posit
|
||||
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
||||
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
||||
}
|
||||
rebuildSpatialIndex();
|
||||
spatialIndexDirty_ = true;
|
||||
}
|
||||
|
||||
void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) {
|
||||
@@ -2507,7 +2511,7 @@ void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tran
|
||||
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
||||
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
||||
}
|
||||
rebuildSpatialIndex();
|
||||
spatialIndexDirty_ = true;
|
||||
}
|
||||
|
||||
void M2Renderer::removeInstance(uint32_t instanceId) {
|
||||
@@ -2595,6 +2599,7 @@ void M2Renderer::rebuildSpatialIndex() {
|
||||
}
|
||||
}
|
||||
}
|
||||
spatialIndexDirty_ = false;
|
||||
}
|
||||
|
||||
void M2Renderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& queryMax,
|
||||
|
||||
@@ -675,6 +675,11 @@ void WMORenderer::removeInstance(uint32_t instanceId) {
|
||||
auto it = std::find_if(instances.begin(), instances.end(),
|
||||
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
|
||||
if (it != instances.end()) {
|
||||
if (m2Renderer_) {
|
||||
for (const auto& doodad : it->doodads) {
|
||||
m2Renderer_->removeInstance(doodad.m2InstanceId);
|
||||
}
|
||||
}
|
||||
instances.erase(it);
|
||||
rebuildSpatialIndex();
|
||||
core::Logger::getInstance().debug("Removed WMO instance ", instanceId);
|
||||
@@ -687,6 +692,17 @@ void WMORenderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> toRemove(instanceIds.begin(), instanceIds.end());
|
||||
if (m2Renderer_) {
|
||||
for (const auto& inst : instances) {
|
||||
if (toRemove.find(inst.id) == toRemove.end()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& doodad : inst.doodads) {
|
||||
m2Renderer_->removeInstance(doodad.m2InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const size_t oldSize = instances.size();
|
||||
instances.erase(std::remove_if(instances.begin(), instances.end(),
|
||||
[&toRemove](const WMOInstance& inst) {
|
||||
@@ -702,6 +718,13 @@ void WMORenderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
|
||||
}
|
||||
|
||||
void WMORenderer::clearInstances() {
|
||||
if (m2Renderer_) {
|
||||
for (const auto& inst : instances) {
|
||||
for (const auto& doodad : inst.doodads) {
|
||||
m2Renderer_->removeInstance(doodad.m2InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
instances.clear();
|
||||
spatialGrid.clear();
|
||||
instanceIndexById.clear();
|
||||
|
||||
Reference in New Issue
Block a user