mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
TransportManager decomposition: - Extract TransportClockSync: server clock offset, yaw flip detection, velocity bootstrap, client/server mode switching - Extract TransportAnimator: spline evaluation, Z clamping, orientation from server yaw or spline tangent - Slim TransportManager to thin orchestrator delegating to ClockSync and Animator; add pushTransform() helper to deduplicate WMO/M2 renderer calls - Remove legacy orientationFromSplineTangent (now uses CatmullRomSpline::orientationFromTangent) Entity path following upgrade: - Replace pathPoints_/pathSegDists_ linear lerp with std::optional<CatmullRomSpline> activeSpline_ - startMoveAlongPath builds SplineKeys with distance-proportional timing - updateMovement evaluates CatmullRomSpline for smooth Catmull-Rom interpolation matching server-side creature movement - Reset activeSpline_ on setPosition/startMoveTo to prevent stale state Tests: - Add test_transport_components (9 cases): ClockSync client/server/reverse modes, yaw flip detection, Animator position eval, server yaw, Z clamping - Link spline.cpp into test_entity for CatmullRomSpline dependency Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
367 lines
15 KiB
C++
367 lines
15 KiB
C++
#include "game/transport_manager.hpp"
|
|
#include "game/transport_clock_sync.hpp"
|
|
#include "game/transport_animator.hpp"
|
|
#include "rendering/wmo_renderer.hpp"
|
|
#include "rendering/m2_renderer.hpp"
|
|
#include "core/coordinates.hpp"
|
|
#include "core/logger.hpp"
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
#include <glm/gtc/constants.hpp>
|
|
#include <glm/gtx/quaternion.hpp>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
namespace wowee::game {
|
|
|
|
TransportManager::TransportManager() = default;
|
|
TransportManager::~TransportManager() = default;
|
|
|
|
void TransportManager::update(float deltaTime) {
|
|
elapsedTime_ += deltaTime;
|
|
|
|
for (auto& [guid, transport] : transports_) {
|
|
// Once we have server clock offset, we can predict server time indefinitely
|
|
// No need for watchdog - keep using the offset even if server updates stop
|
|
updateTransportMovement(transport, deltaTime);
|
|
}
|
|
}
|
|
|
|
void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry) {
|
|
auto* pathEntry = pathRepo_.findPath(pathId);
|
|
if (!pathEntry) {
|
|
LOG_ERROR("TransportManager: Path ", pathId, " not found for transport ", guid);
|
|
return;
|
|
}
|
|
|
|
const auto& spline = pathEntry->spline;
|
|
if (spline.keyCount() == 0) {
|
|
LOG_ERROR("TransportManager: Path ", pathId, " has no waypoints");
|
|
return;
|
|
}
|
|
|
|
ActiveTransport transport;
|
|
transport.guid = guid;
|
|
transport.wmoInstanceId = wmoInstanceId;
|
|
transport.pathId = pathId;
|
|
transport.entry = entry;
|
|
transport.allowBootstrapVelocity = false;
|
|
|
|
// CRITICAL: Set basePosition from spawn position and t=0 offset
|
|
// For stationary paths (1 waypoint), just use spawn position directly
|
|
if (spline.durationMs() == 0 || spline.keyCount() <= 1) {
|
|
// Stationary transport - no path animation
|
|
transport.basePosition = spawnWorldPos;
|
|
transport.position = spawnWorldPos;
|
|
} else if (pathEntry->worldCoords) {
|
|
// World-coordinate path (TaxiPathNode) - points are absolute world positions
|
|
transport.basePosition = glm::vec3(0.0f);
|
|
transport.position = spline.evaluatePosition(0);
|
|
} else {
|
|
// Moving transport - infer base from first path offset
|
|
glm::vec3 offset0 = spline.evaluatePosition(0);
|
|
transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn
|
|
transport.position = spawnWorldPos; // Start at spawn position (base + offset0)
|
|
|
|
// TransportAnimation paths are local offsets; first waypoint is expected near origin.
|
|
// Warn only if the local path itself looks suspicious.
|
|
glm::vec3 firstWaypoint = spline.keys()[0].position;
|
|
if (glm::dot(firstWaypoint, firstWaypoint) > 100.0f) {
|
|
LOG_WARNING("Transport 0x", std::hex, guid, std::dec, " path ", pathId,
|
|
": first local waypoint far from origin: (",
|
|
firstWaypoint.x, ",", firstWaypoint.y, ",", firstWaypoint.z, ")");
|
|
}
|
|
}
|
|
|
|
transport.rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion
|
|
transport.playerOnBoard = false;
|
|
transport.playerLocalOffset = glm::vec3(0.0f);
|
|
transport.hasDeckBounds = false;
|
|
transport.localClockMs = 0;
|
|
transport.hasServerClock = false;
|
|
transport.serverClockOffsetMs = 0;
|
|
// Start with client-side animation for all DBC paths with real movement.
|
|
// If the server sends actual position updates, updateServerTransport() will switch
|
|
// to server-driven mode. This ensures transports like trams (which the server doesn't
|
|
// stream updates for) still animate, while ships/zeppelins switch to server authority.
|
|
transport.useClientAnimation = (pathEntry->fromDBC && spline.durationMs() > 0);
|
|
transport.clientAnimationReverse = false;
|
|
transport.serverYaw = 0.0f;
|
|
transport.hasServerYaw = false;
|
|
transport.serverYawFlipped180 = false;
|
|
transport.serverYawAlignmentScore = 0;
|
|
transport.lastServerUpdate = 0.0f;
|
|
transport.serverUpdateCount = 0;
|
|
transport.serverLinearVelocity = glm::vec3(0.0f);
|
|
transport.serverAngularVelocity = 0.0f;
|
|
transport.hasServerVelocity = false;
|
|
|
|
if (transport.useClientAnimation && spline.durationMs() > 0) {
|
|
// Seed to a stable phase based on our local clock so elevators don't all start at t=0.
|
|
transport.localClockMs = static_cast<uint32_t>(elapsedTime_ * 1000.0) % spline.durationMs();
|
|
LOG_INFO("TransportManager: Enabled client animation for transport 0x",
|
|
std::hex, guid, std::dec, " path=", pathId,
|
|
" durationMs=", spline.durationMs(), " seedMs=", transport.localClockMs,
|
|
(pathEntry->worldCoords ? " [worldCoords]" : (pathEntry->zOnly ? " [z-only]" : "")));
|
|
}
|
|
|
|
updateTransformMatrices(transport);
|
|
|
|
// CRITICAL: Update WMO renderer with initial transform
|
|
pushTransform(transport);
|
|
|
|
transports_[guid] = transport;
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
|
LOG_INFO("TransportManager: Registered transport 0x", std::hex, guid, std::dec,
|
|
" at path ", pathId, " with ", (pathEntry ? pathEntry->spline.keyCount() : 0u), " waypoints",
|
|
" wmoInstanceId=", wmoInstanceId,
|
|
" spawnPos=(", spawnWorldPos.x, ", ", spawnWorldPos.y, ", ", spawnWorldPos.z, ")",
|
|
" basePos=(", transport.basePosition.x, ", ", transport.basePosition.y, ", ", transport.basePosition.z, ")",
|
|
" initialRenderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")");
|
|
}
|
|
|
|
void TransportManager::unregisterTransport(uint64_t guid) {
|
|
transports_.erase(guid);
|
|
LOG_INFO("TransportManager: Unregistered transport ", guid);
|
|
}
|
|
|
|
ActiveTransport* TransportManager::getTransport(uint64_t guid) {
|
|
auto it = transports_.find(guid);
|
|
if (it != transports_.end()) {
|
|
return &it->second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
glm::vec3 TransportManager::getPlayerWorldPosition(uint64_t transportGuid, const glm::vec3& localOffset) {
|
|
auto* transport = getTransport(transportGuid);
|
|
if (!transport) {
|
|
LOG_WARNING("getPlayerWorldPosition: transport 0x", std::hex, transportGuid, std::dec,
|
|
" not found — returning localOffset as-is (callers should guard)");
|
|
return localOffset;
|
|
}
|
|
|
|
if (transport->isM2) {
|
|
// M2 transports (trams): localOffset is a canonical world-space delta
|
|
// from the transport's canonical position. Just add directly.
|
|
return transport->position + localOffset;
|
|
}
|
|
|
|
// WMO transports (ships): localOffset is in transport-local space,
|
|
// use the render-space transform matrix.
|
|
glm::vec4 localPos(localOffset, 1.0f);
|
|
glm::vec4 worldPos = transport->transform * localPos;
|
|
return glm::vec3(worldPos);
|
|
}
|
|
|
|
glm::mat4 TransportManager::getTransportInvTransform(uint64_t transportGuid) {
|
|
auto* transport = getTransport(transportGuid);
|
|
if (!transport) {
|
|
return glm::mat4(1.0f); // Identity fallback
|
|
}
|
|
return transport->invTransform;
|
|
}
|
|
|
|
void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm::vec3>& waypoints, bool looping, float speed) {
|
|
pathRepo_.loadPathFromNodes(pathId, waypoints, looping, speed);
|
|
}
|
|
|
|
void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) {
|
|
auto* transport = getTransport(guid);
|
|
if (!transport) {
|
|
LOG_ERROR("TransportManager: Cannot set deck bounds for unknown transport ", guid);
|
|
return;
|
|
}
|
|
|
|
transport->deckMin = min;
|
|
transport->deckMax = max;
|
|
transport->hasDeckBounds = true;
|
|
}
|
|
|
|
void TransportManager::updateTransportMovement(ActiveTransport& transport, float deltaTime) {
|
|
auto* pathEntry = pathRepo_.findPath(transport.pathId);
|
|
if (!pathEntry) {
|
|
return;
|
|
}
|
|
|
|
const auto& spline = pathEntry->spline;
|
|
if (spline.keyCount() == 0) {
|
|
return;
|
|
}
|
|
|
|
// Stationary transport (durationMs = 0)
|
|
if (spline.durationMs() == 0) {
|
|
// Just update transform (position already set)
|
|
updateTransformMatrices(transport);
|
|
pushTransform(transport);
|
|
return;
|
|
}
|
|
|
|
// Compute path time via ClockSync
|
|
uint32_t pathTimeMs = 0;
|
|
if (!clockSync_.computePathTime(transport, spline, elapsedTime_, deltaTime, pathTimeMs)) {
|
|
// Strict server-authoritative mode: do not guess movement between server snapshots.
|
|
updateTransformMatrices(transport);
|
|
pushTransform(transport);
|
|
return;
|
|
}
|
|
|
|
// Evaluate position + rotation via Animator
|
|
animator_.evaluateAndApply(transport, *pathEntry, pathTimeMs);
|
|
|
|
// Update transform matrices
|
|
updateTransformMatrices(transport);
|
|
pushTransform(transport);
|
|
|
|
// Debug logging every 600 frames (~10 seconds at 60fps)
|
|
static int debugFrameCount = 0;
|
|
if (debugFrameCount++ % 600 == 0) {
|
|
LOG_DEBUG("Transport 0x", std::hex, transport.guid, std::dec,
|
|
" pathTime=", pathTimeMs, "ms / ", spline.durationMs(), "ms",
|
|
" pos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")",
|
|
" mode=", (transport.useClientAnimation ? "client" : "server"),
|
|
" isM2=", transport.isM2);
|
|
}
|
|
}
|
|
|
|
// Push transform to the appropriate renderer (WMO or M2).
|
|
void TransportManager::pushTransform(ActiveTransport& transport) {
|
|
if (transport.isM2) {
|
|
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
|
} else {
|
|
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
|
}
|
|
}
|
|
|
|
void TransportManager::updateTransformMatrices(ActiveTransport& transport) {
|
|
// Convert position from canonical to render coordinates for WMO rendering
|
|
// Canonical: +X=North, +Y=West, +Z=Up
|
|
// Render: renderX=wowY (west), renderY=wowX (north), renderZ=wowZ (up)
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
|
|
|
// Convert rotation from canonical to render space using proper basis change
|
|
// Canonical → Render is a 90° CCW rotation around Z (swaps X and Y)
|
|
// Proper formula: q_render = q_basis * q_canonical * q_basis^-1
|
|
glm::quat basisRotation = glm::angleAxis(glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
|
|
glm::quat basisInverse = glm::conjugate(basisRotation);
|
|
glm::quat renderRot = basisRotation * transport.rotation * basisInverse;
|
|
|
|
// Build transform matrix: translate * rotate * scale
|
|
glm::mat4 translation = glm::translate(glm::mat4(1.0f), renderPos);
|
|
glm::mat4 rotation = glm::mat4_cast(renderRot);
|
|
glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(1.0f)); // No scaling for transports
|
|
|
|
transport.transform = translation * rotation * scale;
|
|
transport.invTransform = glm::inverse(transport.transform);
|
|
}
|
|
|
|
void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& position, float orientation) {
|
|
auto* transport = getTransport(guid);
|
|
if (!transport) {
|
|
LOG_WARNING("TransportManager::updateServerTransport: Transport not found: 0x", std::hex, guid, std::dec);
|
|
return;
|
|
}
|
|
|
|
auto* pathEntry = pathRepo_.findPath(transport->pathId);
|
|
|
|
if (!pathEntry || pathEntry->spline.durationMs() == 0) {
|
|
// No path or stationary — handle directly before delegating to ClockSync.
|
|
// Still track update count so future path assignments work.
|
|
transport->serverUpdateCount++;
|
|
transport->lastServerUpdate = elapsedTime_;
|
|
transport->basePosition = position;
|
|
transport->position = position;
|
|
transport->rotation = glm::angleAxis(orientation, glm::vec3(0.0f, 0.0f, 1.0f));
|
|
updateTransformMatrices(*transport);
|
|
pushTransform(*transport);
|
|
return;
|
|
}
|
|
|
|
// Delegate clock sync, yaw correction, and velocity bootstrap to ClockSync.
|
|
clockSync_.processServerUpdate(*transport, pathEntry, position, orientation, elapsedTime_);
|
|
|
|
updateTransformMatrices(*transport);
|
|
pushTransform(*transport);
|
|
}
|
|
|
|
bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) {
|
|
return pathRepo_.loadTransportAnimationDBC(assetMgr);
|
|
}
|
|
|
|
bool TransportManager::loadTaxiPathNodeDBC(pipeline::AssetManager* assetMgr) {
|
|
return pathRepo_.loadTaxiPathNodeDBC(assetMgr);
|
|
}
|
|
|
|
bool TransportManager::hasTaxiPath(uint32_t taxiPathId) const {
|
|
return pathRepo_.hasTaxiPath(taxiPathId);
|
|
}
|
|
|
|
bool TransportManager::assignTaxiPathToTransport(uint32_t entry, uint32_t taxiPathId) {
|
|
auto* taxiEntry = pathRepo_.findTaxiPath(taxiPathId);
|
|
if (!taxiEntry) {
|
|
LOG_WARNING("No TaxiPathNode path for taxiPathId=", taxiPathId);
|
|
return false;
|
|
}
|
|
|
|
// Find transport(s) with matching entry that are at (0,0,0)
|
|
for (auto& [guid, transport] : transports_) {
|
|
if (transport.entry != entry) continue;
|
|
if (glm::dot(transport.position, transport.position) > 1.0f) continue; // Already has real position
|
|
|
|
// Copy the taxi path into the main paths (indexed by GO entry for this transport)
|
|
PathEntry copied(taxiEntry->spline, entry, taxiEntry->zOnly, taxiEntry->fromDBC, taxiEntry->worldCoords);
|
|
pathRepo_.storePath(entry, std::move(copied));
|
|
|
|
auto* storedEntry = pathRepo_.findPath(entry);
|
|
|
|
// Update transport to use the new path
|
|
transport.pathId = entry;
|
|
transport.basePosition = glm::vec3(0.0f); // World-coordinate path, no base offset
|
|
if (storedEntry && storedEntry->spline.keyCount() > 0) {
|
|
transport.position = storedEntry->spline.evaluatePosition(0);
|
|
}
|
|
transport.useClientAnimation = true; // Server won't send position updates
|
|
|
|
// Seed local clock to a deterministic phase
|
|
if (storedEntry && storedEntry->spline.durationMs() > 0) {
|
|
transport.localClockMs = static_cast<uint32_t>(elapsedTime_ * 1000.0) % storedEntry->spline.durationMs();
|
|
}
|
|
|
|
updateTransformMatrices(transport);
|
|
pushTransform(transport);
|
|
|
|
LOG_INFO("Assigned TaxiPathNode path to transport 0x", std::hex, guid, std::dec,
|
|
" entry=", entry, " taxiPathId=", taxiPathId,
|
|
" waypoints=", storedEntry ? storedEntry->spline.keyCount() : 0u,
|
|
" duration=", storedEntry ? storedEntry->spline.durationMs() : 0u, "ms",
|
|
" startPos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")");
|
|
return true;
|
|
}
|
|
|
|
LOG_DEBUG("No transport at (0,0,0) found for entry=", entry, " taxiPathId=", taxiPathId);
|
|
return false;
|
|
}
|
|
|
|
bool TransportManager::hasPathForEntry(uint32_t entry) const {
|
|
return pathRepo_.hasPathForEntry(entry);
|
|
}
|
|
|
|
bool TransportManager::hasUsableMovingPathForEntry(uint32_t entry, float minXYRange) const {
|
|
return pathRepo_.hasUsableMovingPathForEntry(entry, minXYRange);
|
|
}
|
|
|
|
uint32_t TransportManager::inferDbcPathForSpawn(const glm::vec3& spawnWorldPos,
|
|
float maxDistance,
|
|
bool allowZOnly) const {
|
|
return pathRepo_.inferDbcPathForSpawn(spawnWorldPos, maxDistance, allowZOnly);
|
|
}
|
|
|
|
uint32_t TransportManager::inferMovingPathForSpawn(const glm::vec3& spawnWorldPos, float maxDistance) const {
|
|
return pathRepo_.inferMovingPathForSpawn(spawnWorldPos, maxDistance);
|
|
}
|
|
|
|
uint32_t TransportManager::pickFallbackMovingPath(uint32_t entry, uint32_t displayId) const {
|
|
return pathRepo_.pickFallbackMovingPath(entry, displayId);
|
|
}
|
|
|
|
} // namespace wowee::game
|