mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-16 01:03:51 +00:00
refactor: decompose TransportManager and upgrade Entity to CatmullRom splines
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>
This commit is contained in:
parent
de0383aa6b
commit
39719cac82
10 changed files with 704 additions and 337 deletions
|
|
@ -547,6 +547,8 @@ set(WOWEE_SOURCES
|
|||
src/game/warden_memory.cpp
|
||||
src/game/transport_manager.cpp
|
||||
src/game/transport_path_repository.cpp
|
||||
src/game/transport_clock_sync.cpp
|
||||
src/game/transport_animator.cpp
|
||||
src/game/world.cpp
|
||||
src/game/player.cpp
|
||||
src/game/entity.cpp
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include "math/spline.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
|
@ -80,31 +82,41 @@ public:
|
|||
orientation = o;
|
||||
isMoving_ = false; // Instant position set cancels interpolation
|
||||
usePathMode_ = false;
|
||||
activeSpline_.reset();
|
||||
}
|
||||
|
||||
// Multi-segment path movement
|
||||
// Multi-segment path movement (Catmull-Rom spline interpolation)
|
||||
void startMoveAlongPath(const std::vector<std::array<float, 3>>& path, float destO, float totalDuration) {
|
||||
if (path.empty()) return;
|
||||
if (path.size() == 1 || totalDuration <= 0.0f) {
|
||||
startMoveTo(path.back()[0], path.back()[1], path.back()[2], destO, totalDuration);
|
||||
return;
|
||||
}
|
||||
// Compute cumulative distances for proportional segment timing
|
||||
pathPoints_ = path;
|
||||
pathSegDists_.resize(path.size());
|
||||
pathSegDists_[0] = 0.0f;
|
||||
// Build cumulative distances for proportional time assignment
|
||||
std::vector<float> cumDist(path.size(), 0.0f);
|
||||
float totalDist = 0.0f;
|
||||
for (size_t i = 1; i < path.size(); i++) {
|
||||
float dx = path[i][0] - path[i - 1][0];
|
||||
float dy = path[i][1] - path[i - 1][1];
|
||||
float dz = path[i][2] - path[i - 1][2];
|
||||
totalDist += std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
pathSegDists_[i] = totalDist;
|
||||
cumDist[i] = totalDist;
|
||||
}
|
||||
if (totalDist < 0.001f) {
|
||||
startMoveTo(path.back()[0], path.back()[1], path.back()[2], destO, totalDuration);
|
||||
return;
|
||||
}
|
||||
// Build SplineKeys with distance-proportional time
|
||||
uint32_t durationMs = static_cast<uint32_t>(totalDuration * 1000.0f);
|
||||
std::vector<math::SplineKey> keys(path.size());
|
||||
for (size_t i = 0; i < path.size(); i++) {
|
||||
float fraction = cumDist[i] / totalDist;
|
||||
keys[i].timeMs = static_cast<uint32_t>(fraction * durationMs);
|
||||
keys[i].position = {path[i][0], path[i][1], path[i][2]};
|
||||
}
|
||||
activeSpline_.emplace(std::move(keys), /*timeClosed=*/false);
|
||||
splineDurationMs_ = durationMs;
|
||||
|
||||
// Snap position if in overrun phase
|
||||
if (isMoving_ && moveElapsed_ >= moveDuration_) {
|
||||
x = moveEndX_; y = moveEndY_; z = moveEndZ_;
|
||||
|
|
@ -130,6 +142,7 @@ public:
|
|||
// Movement interpolation (syncs entity position with renderer during movement)
|
||||
void startMoveTo(float destX, float destY, float destZ, float destO, float durationSec) {
|
||||
usePathMode_ = false;
|
||||
activeSpline_.reset();
|
||||
if (durationSec <= 0.0f) {
|
||||
setPosition(destX, destY, destZ, destO);
|
||||
return;
|
||||
|
|
@ -172,24 +185,14 @@ public:
|
|||
if (!isMoving_) return;
|
||||
moveElapsed_ += deltaTime;
|
||||
if (moveElapsed_ < moveDuration_) {
|
||||
if (usePathMode_ && pathPoints_.size() > 1) {
|
||||
// Multi-segment path interpolation
|
||||
float totalDist = pathSegDists_.back();
|
||||
float t = moveElapsed_ / moveDuration_;
|
||||
float targetDist = t * totalDist;
|
||||
// Find the segment containing targetDist
|
||||
size_t seg = 1;
|
||||
while (seg < pathSegDists_.size() - 1 && pathSegDists_[seg] < targetDist)
|
||||
seg++;
|
||||
float segStart = pathSegDists_[seg - 1];
|
||||
float segEnd = pathSegDists_[seg];
|
||||
float segLen = segEnd - segStart;
|
||||
float segT = (segLen > 0.001f) ? (targetDist - segStart) / segLen : 0.0f;
|
||||
const auto& p0 = pathPoints_[seg - 1];
|
||||
const auto& p1 = pathPoints_[seg];
|
||||
x = p0[0] + (p1[0] - p0[0]) * segT;
|
||||
y = p0[1] + (p1[1] - p0[1]) * segT;
|
||||
z = p0[2] + (p1[2] - p0[2]) * segT;
|
||||
if (usePathMode_ && activeSpline_) {
|
||||
// Catmull-Rom spline interpolation
|
||||
uint32_t pathTimeMs = static_cast<uint32_t>(moveElapsed_ * 1000.0f);
|
||||
if (pathTimeMs >= splineDurationMs_) pathTimeMs = splineDurationMs_ - 1;
|
||||
glm::vec3 pos = activeSpline_->evaluatePosition(pathTimeMs);
|
||||
x = pos.x;
|
||||
y = pos.y;
|
||||
z = pos.z;
|
||||
} else {
|
||||
// Single-segment linear interpolation
|
||||
float t = moveElapsed_ / moveDuration_;
|
||||
|
|
@ -277,9 +280,9 @@ protected:
|
|||
float moveDuration_ = 0;
|
||||
float moveElapsed_ = 0;
|
||||
float velX_ = 0, velY_ = 0, velZ_ = 0; // Smoothed velocity for dead reckoning
|
||||
// Multi-segment path data
|
||||
std::vector<std::array<float, 3>> pathPoints_;
|
||||
std::vector<float> pathSegDists_; // Cumulative distances for each waypoint
|
||||
// CatmullRom spline for multi-segment path movement (replaces linear pathPoints_/pathSegDists_)
|
||||
std::optional<math::CatmullRomSpline> activeSpline_;
|
||||
uint32_t splineDurationMs_ = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
33
include/game/transport_animator.hpp
Normal file
33
include/game/transport_animator.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// include/game/transport_animator.hpp
|
||||
// Path evaluation, Z clamping, and orientation for transports.
|
||||
// Extracted from TransportManager (Phase 3b of spline refactoring).
|
||||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee::math { class CatmullRomSpline; }
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
struct ActiveTransport;
|
||||
struct PathEntry;
|
||||
|
||||
/// Evaluates a transport's position and orientation from a spline path and path time.
|
||||
class TransportAnimator {
|
||||
public:
|
||||
/// Evaluate the spline at pathTimeMs, apply Z clamping, and update
|
||||
/// transport.position and transport.rotation in-place.
|
||||
void evaluateAndApply(
|
||||
ActiveTransport& transport,
|
||||
const PathEntry& pathEntry,
|
||||
uint32_t pathTimeMs) const;
|
||||
|
||||
private:
|
||||
/// Guard against bad fallback Z offsets on non-world-coordinate paths.
|
||||
static float clampZOffset(float z, bool worldCoords, bool clientAnim,
|
||||
int serverUpdateCount, bool hasServerClock);
|
||||
};
|
||||
|
||||
} // namespace wowee::game
|
||||
49
include/game/transport_clock_sync.hpp
Normal file
49
include/game/transport_clock_sync.hpp
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// include/game/transport_clock_sync.hpp
|
||||
// Clock synchronization and yaw correction for transports.
|
||||
// Extracted from TransportManager (Phase 3a of spline refactoring).
|
||||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee::math { class CatmullRomSpline; }
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
struct ActiveTransport;
|
||||
struct PathEntry;
|
||||
|
||||
/// Manages clock sync, server-yaw correction, and velocity bootstrap for a single transport.
|
||||
class TransportClockSync {
|
||||
public:
|
||||
/// Compute pathTimeMs for a transport given current elapsed time and deltaTime.
|
||||
/// Returns false if the transport should use raw server position (no interpolation).
|
||||
[[nodiscard]] bool computePathTime(
|
||||
ActiveTransport& transport,
|
||||
const math::CatmullRomSpline& spline,
|
||||
double elapsedTime,
|
||||
float deltaTime,
|
||||
uint32_t& outPathTimeMs) const;
|
||||
|
||||
/// Process a server position update: update clock offset, detect yaw flips,
|
||||
/// bootstrap velocity, and switch between client/server driven modes.
|
||||
void processServerUpdate(
|
||||
ActiveTransport& transport,
|
||||
const PathEntry* pathEntry,
|
||||
const glm::vec3& position,
|
||||
float orientation,
|
||||
double elapsedTime);
|
||||
|
||||
private:
|
||||
/// Detect and apply 180-degree yaw correction based on movement vs heading alignment.
|
||||
void updateYawAlignment(
|
||||
ActiveTransport& transport,
|
||||
const glm::vec3& velocity) const;
|
||||
|
||||
/// Bootstrap velocity from nearest DBC path segment on first authoritative sample.
|
||||
void bootstrapVelocityFromPath(
|
||||
ActiveTransport& transport,
|
||||
const PathEntry& pathEntry) const;
|
||||
};
|
||||
|
||||
} // namespace wowee::game
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include "game/transport_path_repository.hpp"
|
||||
#include "game/transport_clock_sync.hpp"
|
||||
#include "game/transport_animator.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
|
@ -124,10 +126,11 @@ public:
|
|||
private:
|
||||
void updateTransportMovement(ActiveTransport& transport, float deltaTime);
|
||||
void updateTransformMatrices(ActiveTransport& transport);
|
||||
/// Legacy transport orientation from tangent (preserves original cross-product order).
|
||||
static glm::quat orientationFromSplineTangent(const glm::vec3& tangent);
|
||||
void pushTransform(ActiveTransport& transport);
|
||||
|
||||
TransportPathRepository pathRepo_;
|
||||
TransportClockSync clockSync_;
|
||||
TransportAnimator animator_;
|
||||
std::unordered_map<uint64_t, ActiveTransport> transports_;
|
||||
rendering::WMORenderer* wmoRenderer_ = nullptr;
|
||||
rendering::M2Renderer* m2Renderer_ = nullptr;
|
||||
|
|
|
|||
64
src/game/transport_animator.cpp
Normal file
64
src/game/transport_animator.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// src/game/transport_animator.cpp
|
||||
// Path evaluation, Z clamping, and orientation for transports.
|
||||
// Extracted from TransportManager::updateTransportMovement (Phase 3b of spline refactoring).
|
||||
#include "game/transport_animator.hpp"
|
||||
#include "game/transport_manager.hpp"
|
||||
#include "game/transport_path_repository.hpp"
|
||||
#include "math/spline.hpp"
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
void TransportAnimator::evaluateAndApply(
|
||||
ActiveTransport& transport,
|
||||
const PathEntry& pathEntry,
|
||||
uint32_t pathTimeMs) const
|
||||
{
|
||||
const auto& spline = pathEntry.spline;
|
||||
|
||||
// Evaluate position from time via CatmullRomSpline (path is local offsets, add base position)
|
||||
glm::vec3 pathOffset = spline.evaluatePosition(pathTimeMs);
|
||||
|
||||
pathOffset.z = clampZOffset(
|
||||
pathOffset.z,
|
||||
pathEntry.worldCoords,
|
||||
transport.useClientAnimation,
|
||||
transport.serverUpdateCount,
|
||||
transport.hasServerClock);
|
||||
|
||||
transport.position = transport.basePosition + pathOffset;
|
||||
|
||||
// Use server yaw if available (authoritative), otherwise compute from spline tangent
|
||||
if (transport.hasServerYaw) {
|
||||
float effectiveYaw = transport.serverYaw +
|
||||
(transport.serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport.rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
} else {
|
||||
auto result = spline.evaluate(pathTimeMs);
|
||||
transport.rotation = math::CatmullRomSpline::orientationFromTangent(result.tangent);
|
||||
}
|
||||
}
|
||||
|
||||
float TransportAnimator::clampZOffset(float z, bool worldCoords, bool clientAnim,
|
||||
int serverUpdateCount, bool hasServerClock)
|
||||
{
|
||||
// Skip Z clamping for world-coordinate paths (TaxiPathNode) where values are absolute positions.
|
||||
if (worldCoords) return z;
|
||||
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
constexpr float kMaxFallbackZOffset = 8.0f;
|
||||
|
||||
// Clamp fallback Z offsets for non-world-coordinate paths to prevent transport
|
||||
// models from sinking below sea level on paths derived only from spawn-time data
|
||||
// (notably icebreaker routes where the DBC path has steep vertical curves).
|
||||
if (clientAnim && serverUpdateCount <= 1) {
|
||||
z = std::max(z, kMinFallbackZOffset);
|
||||
}
|
||||
if (!clientAnim && !hasServerClock) {
|
||||
z = std::clamp(z, kMinFallbackZOffset, kMaxFallbackZOffset);
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
} // namespace wowee::game
|
||||
272
src/game/transport_clock_sync.cpp
Normal file
272
src/game/transport_clock_sync.cpp
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
// src/game/transport_clock_sync.cpp
|
||||
// Clock synchronization and yaw correction for transports.
|
||||
// Extracted from TransportManager (Phase 3a of spline refactoring).
|
||||
#include "game/transport_clock_sync.hpp"
|
||||
#include "game/transport_manager.hpp"
|
||||
#include "game/transport_path_repository.hpp"
|
||||
#include "math/spline.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
bool TransportClockSync::computePathTime(
|
||||
ActiveTransport& transport,
|
||||
const math::CatmullRomSpline& spline,
|
||||
double elapsedTime,
|
||||
float deltaTime,
|
||||
uint32_t& outPathTimeMs) const
|
||||
{
|
||||
uint32_t nowMs = static_cast<uint32_t>(elapsedTime * 1000.0);
|
||||
uint32_t durationMs = spline.durationMs();
|
||||
|
||||
if (transport.hasServerClock) {
|
||||
// Predict server time using clock offset (works for both client and server-driven modes)
|
||||
int64_t serverTimeMs = static_cast<int64_t>(nowMs) + transport.serverClockOffsetMs;
|
||||
int64_t mod = static_cast<int64_t>(durationMs);
|
||||
int64_t wrapped = serverTimeMs % mod;
|
||||
if (wrapped < 0) wrapped += mod;
|
||||
outPathTimeMs = static_cast<uint32_t>(wrapped);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (transport.useClientAnimation) {
|
||||
// Pure local clock (no server sync yet, client-driven)
|
||||
uint32_t dtMs = static_cast<uint32_t>(deltaTime * 1000.0f);
|
||||
if (!transport.clientAnimationReverse) {
|
||||
transport.localClockMs += dtMs;
|
||||
} else {
|
||||
if (dtMs > durationMs) {
|
||||
dtMs %= durationMs;
|
||||
}
|
||||
if (transport.localClockMs >= dtMs) {
|
||||
transport.localClockMs -= dtMs;
|
||||
} else {
|
||||
transport.localClockMs = durationMs - (dtMs - transport.localClockMs);
|
||||
}
|
||||
}
|
||||
outPathTimeMs = transport.localClockMs % durationMs;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strict server-authoritative mode: do not guess movement between server snapshots.
|
||||
return false;
|
||||
}
|
||||
|
||||
void TransportClockSync::processServerUpdate(
|
||||
ActiveTransport& transport,
|
||||
const PathEntry* pathEntry,
|
||||
const glm::vec3& position,
|
||||
float orientation,
|
||||
double elapsedTime)
|
||||
{
|
||||
const bool hadPrevUpdate = (transport.serverUpdateCount > 0);
|
||||
const double prevUpdateTime = transport.lastServerUpdate;
|
||||
const glm::vec3 prevPos = transport.position;
|
||||
|
||||
const bool hasPath = (pathEntry != nullptr);
|
||||
const bool isZOnlyPath = (hasPath && pathEntry->fromDBC && pathEntry->zOnly && pathEntry->spline.durationMs() > 0);
|
||||
const bool isWorldCoordPath = (hasPath && pathEntry->worldCoords && pathEntry->spline.durationMs() > 0);
|
||||
|
||||
// Don't let (0,0,0) server updates override a TaxiPathNode world-coordinate path
|
||||
if (isWorldCoordPath && glm::dot(position, position) < 1.0f) {
|
||||
transport.serverUpdateCount++;
|
||||
transport.lastServerUpdate = elapsedTime;
|
||||
transport.serverYaw = orientation;
|
||||
transport.hasServerYaw = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Track server updates
|
||||
transport.serverUpdateCount++;
|
||||
transport.lastServerUpdate = elapsedTime;
|
||||
// Z-only elevators and world-coordinate paths (TaxiPathNode) always stay client-driven.
|
||||
// For other DBC paths (trams, ships): only switch to server-driven mode when the server
|
||||
// sends a position that actually differs from the current position, indicating it's
|
||||
// actively streaming movement data (not just echoing the spawn position).
|
||||
if (isZOnlyPath || isWorldCoordPath) {
|
||||
transport.useClientAnimation = true;
|
||||
} else if (transport.useClientAnimation && hasPath && pathEntry->fromDBC) {
|
||||
glm::vec3 pd = position - transport.position;
|
||||
float posDeltaSq = glm::dot(pd, pd);
|
||||
if (posDeltaSq > 1.0f) {
|
||||
// Server sent a meaningfully different position — it's actively driving this transport
|
||||
transport.useClientAnimation = false;
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" switching to server-driven (posDeltaSq=", posDeltaSq, ")");
|
||||
}
|
||||
// Otherwise keep client animation (server just echoed spawn pos or sent small jitter)
|
||||
} else if (!hasPath || !pathEntry->fromDBC) {
|
||||
// No DBC path — purely server-driven
|
||||
transport.useClientAnimation = false;
|
||||
}
|
||||
transport.clientAnimationReverse = false;
|
||||
|
||||
// Server-authoritative transport mode:
|
||||
// Trust explicit server world position/orientation directly for all moving transports.
|
||||
transport.hasServerClock = false;
|
||||
if (transport.serverUpdateCount == 1) {
|
||||
// Seed once from first authoritative update; keep stable base for fallback phase estimation.
|
||||
// For z-only elevator paths, keep the spawn-derived basePosition (the DBC path is local offsets).
|
||||
if (!isZOnlyPath) {
|
||||
transport.basePosition = position;
|
||||
}
|
||||
}
|
||||
transport.position = position;
|
||||
transport.serverYaw = orientation;
|
||||
transport.hasServerYaw = true;
|
||||
float effectiveYaw = transport.serverYaw + (transport.serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport.rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
||||
if (hadPrevUpdate) {
|
||||
const double dt = elapsedTime - prevUpdateTime;
|
||||
if (dt > 0.001) {
|
||||
glm::vec3 v = (position - prevPos) / static_cast<float>(dt);
|
||||
float speedSq = glm::dot(v, v);
|
||||
constexpr float kMinAuthoritativeSpeed = 0.15f;
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
if (speedSq >= kMinAuthoritativeSpeed * kMinAuthoritativeSpeed) {
|
||||
updateYawAlignment(transport, v);
|
||||
|
||||
if (speedSq > kMaxSpeed * kMaxSpeed) {
|
||||
v *= (kMaxSpeed * glm::inversesqrt(speedSq));
|
||||
}
|
||||
|
||||
transport.serverLinearVelocity = v;
|
||||
transport.serverAngularVelocity = 0.0f;
|
||||
transport.hasServerVelocity = true;
|
||||
|
||||
// Re-apply potentially corrected yaw this frame after alignment check.
|
||||
effectiveYaw = transport.serverYaw + (transport.serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport.rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Seed fallback path phase from nearest waypoint to the first authoritative sample.
|
||||
if (pathEntry && pathEntry->spline.keyCount() > 0 && pathEntry->spline.durationMs() > 0) {
|
||||
glm::vec3 local = position - transport.basePosition;
|
||||
size_t bestIdx = pathEntry->spline.findNearestKey(local);
|
||||
transport.localClockMs = pathEntry->spline.keys()[bestIdx].timeMs % pathEntry->spline.durationMs();
|
||||
}
|
||||
|
||||
// Bootstrap velocity from mapped DBC path on first authoritative sample.
|
||||
if (transport.allowBootstrapVelocity && pathEntry && pathEntry->spline.keyCount() >= 2 && pathEntry->spline.durationMs() > 0) {
|
||||
bootstrapVelocityFromPath(transport, *pathEntry);
|
||||
} else if (!transport.allowBootstrapVelocity) {
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" DBC bootstrap velocity disabled for this transport");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TransportClockSync::updateYawAlignment(
|
||||
ActiveTransport& transport,
|
||||
const glm::vec3& velocity) const
|
||||
{
|
||||
// Auto-detect 180-degree yaw mismatch by comparing heading to movement direction.
|
||||
// Some transports appear to report yaw opposite their actual travel direction.
|
||||
glm::vec2 horizontalV(velocity.x, velocity.y);
|
||||
float hLenSq = glm::dot(horizontalV, horizontalV);
|
||||
if (hLenSq > 0.04f) {
|
||||
horizontalV *= glm::inversesqrt(hLenSq);
|
||||
glm::vec2 heading(std::cos(transport.serverYaw), std::sin(transport.serverYaw));
|
||||
float alignDot = glm::dot(heading, horizontalV);
|
||||
|
||||
if (alignDot < -0.35f) {
|
||||
transport.serverYawAlignmentScore = std::max(transport.serverYawAlignmentScore - 1, -12);
|
||||
} else if (alignDot > 0.35f) {
|
||||
transport.serverYawAlignmentScore = std::min(transport.serverYawAlignmentScore + 1, 12);
|
||||
}
|
||||
|
||||
if (!transport.serverYawFlipped180 && transport.serverYawAlignmentScore <= -4) {
|
||||
transport.serverYawFlipped180 = true;
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" enabled 180-degree yaw correction (alignScore=",
|
||||
transport.serverYawAlignmentScore, ")");
|
||||
} else if (transport.serverYawFlipped180 &&
|
||||
transport.serverYawAlignmentScore >= 4) {
|
||||
transport.serverYawFlipped180 = false;
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" disabled 180-degree yaw correction (alignScore=",
|
||||
transport.serverYawAlignmentScore, ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TransportClockSync::bootstrapVelocityFromPath(
|
||||
ActiveTransport& transport,
|
||||
const PathEntry& pathEntry) const
|
||||
{
|
||||
// Bootstrap velocity from nearest DBC path segment on first authoritative sample.
|
||||
// This avoids "stalled at dock" when server sends sparse transport snapshots.
|
||||
const auto& keys = pathEntry.spline.keys();
|
||||
glm::vec3 local = transport.position - transport.basePosition;
|
||||
size_t bestIdx = pathEntry.spline.findNearestKey(local);
|
||||
|
||||
float bestDistSq = 0.0f;
|
||||
{
|
||||
glm::vec3 d = keys[bestIdx].position - local;
|
||||
bestDistSq = glm::dot(d, d);
|
||||
}
|
||||
|
||||
constexpr float kMaxBootstrapNearestDist = 80.0f;
|
||||
if (bestDistSq > (kMaxBootstrapNearestDist * kMaxBootstrapNearestDist)) {
|
||||
LOG_WARNING("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" skipping DBC bootstrap velocity: nearest path point too far (dist=",
|
||||
std::sqrt(bestDistSq), ", path=", transport.pathId, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t n = keys.size();
|
||||
uint32_t durMs = pathEntry.spline.durationMs();
|
||||
constexpr float kMinBootstrapSpeed = 0.25f;
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
|
||||
auto tryApplySegment = [&](size_t a, size_t b) {
|
||||
uint32_t t0 = keys[a].timeMs;
|
||||
uint32_t t1 = keys[b].timeMs;
|
||||
if (b == 0 && t1 <= t0 && durMs > 0) {
|
||||
t1 = durMs;
|
||||
}
|
||||
if (t1 <= t0) return;
|
||||
glm::vec3 seg = keys[b].position - keys[a].position;
|
||||
float dtSeg = static_cast<float>(t1 - t0) / 1000.0f;
|
||||
if (dtSeg <= 0.001f) return;
|
||||
glm::vec3 v = seg / dtSeg;
|
||||
float speedSq = glm::dot(v, v);
|
||||
if (speedSq < kMinBootstrapSpeed * kMinBootstrapSpeed) return;
|
||||
if (speedSq > kMaxSpeed * kMaxSpeed) {
|
||||
v *= (kMaxSpeed * glm::inversesqrt(speedSq));
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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) {
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" bootstrapped velocity from DBC path ", transport.pathId,
|
||||
" v=(", transport.serverLinearVelocity.x, ", ",
|
||||
transport.serverLinearVelocity.y, ", ",
|
||||
transport.serverLinearVelocity.z, ")");
|
||||
} else {
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" skipped DBC bootstrap velocity (segment too short/static)");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace wowee::game
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#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"
|
||||
|
|
@ -105,11 +107,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
updateTransformMatrices(transport);
|
||||
|
||||
// CRITICAL: Update WMO renderer with initial transform
|
||||
if (transport.isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
pushTransform(transport);
|
||||
|
||||
transports_[guid] = transport;
|
||||
|
||||
|
|
@ -195,129 +193,44 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
if (spline.durationMs() == 0) {
|
||||
// Just update transform (position already set)
|
||||
updateTransformMatrices(transport);
|
||||
if (transport.isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
pushTransform(transport);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate path time
|
||||
uint32_t nowMs = static_cast<uint32_t>(elapsedTime_ * 1000.0);
|
||||
// Compute path time via ClockSync
|
||||
uint32_t pathTimeMs = 0;
|
||||
uint32_t durationMs = spline.durationMs();
|
||||
|
||||
if (transport.hasServerClock) {
|
||||
// Predict server time using clock offset (works for both client and server-driven modes)
|
||||
int64_t serverTimeMs = static_cast<int64_t>(nowMs) + transport.serverClockOffsetMs;
|
||||
int64_t mod = static_cast<int64_t>(durationMs);
|
||||
int64_t wrapped = serverTimeMs % mod;
|
||||
if (wrapped < 0) wrapped += mod;
|
||||
pathTimeMs = static_cast<uint32_t>(wrapped);
|
||||
} else if (transport.useClientAnimation) {
|
||||
// Pure local clock (no server sync yet, client-driven)
|
||||
uint32_t dtMs = static_cast<uint32_t>(deltaTime * 1000.0f);
|
||||
if (!transport.clientAnimationReverse) {
|
||||
transport.localClockMs += dtMs;
|
||||
} else {
|
||||
if (dtMs > durationMs) {
|
||||
dtMs %= durationMs;
|
||||
}
|
||||
if (transport.localClockMs >= dtMs) {
|
||||
transport.localClockMs -= dtMs;
|
||||
} else {
|
||||
transport.localClockMs = durationMs - (dtMs - transport.localClockMs);
|
||||
}
|
||||
}
|
||||
pathTimeMs = transport.localClockMs % durationMs;
|
||||
} else {
|
||||
if (!clockSync_.computePathTime(transport, spline, elapsedTime_, deltaTime, pathTimeMs)) {
|
||||
// Strict server-authoritative mode: do not guess movement between server snapshots.
|
||||
updateTransformMatrices(transport);
|
||||
if (transport.isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
pushTransform(transport);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate position from time via CatmullRomSpline (path is local offsets, add base position)
|
||||
glm::vec3 pathOffset = spline.evaluatePosition(pathTimeMs);
|
||||
// Guard against bad fallback Z curves on some remapped transport paths (notably icebreakers),
|
||||
// where path offsets can sink far below sea level when we only have spawn-time data.
|
||||
// Skip Z clamping for world-coordinate paths (TaxiPathNode) where values are absolute positions.
|
||||
// Clamp fallback Z offsets for non-world-coordinate paths to prevent transport
|
||||
// models from sinking below sea level on paths derived only from spawn-time data
|
||||
// (notably icebreaker routes where the DBC path has steep vertical curves).
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
constexpr float kMaxFallbackZOffset = 8.0f;
|
||||
if (!pathEntry->worldCoords) {
|
||||
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
|
||||
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
|
||||
}
|
||||
if (!transport.useClientAnimation && !transport.hasServerClock) {
|
||||
pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset);
|
||||
}
|
||||
}
|
||||
transport.position = transport.basePosition + pathOffset;
|
||||
|
||||
// Use server yaw if available (authoritative), otherwise compute from spline tangent
|
||||
if (transport.hasServerYaw) {
|
||||
float effectiveYaw = transport.serverYaw + (transport.serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport.rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
} else {
|
||||
auto result = spline.evaluate(pathTimeMs);
|
||||
transport.rotation = orientationFromSplineTangent(result.tangent);
|
||||
}
|
||||
// Evaluate position + rotation via Animator
|
||||
animator_.evaluateAndApply(transport, *pathEntry, pathTimeMs);
|
||||
|
||||
// Update transform matrices
|
||||
updateTransformMatrices(transport);
|
||||
|
||||
// Update WMO instance position
|
||||
if (transport.isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
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 / ", durationMs, "ms",
|
||||
" pathTime=", pathTimeMs, "ms / ", spline.durationMs(), "ms",
|
||||
" pos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")",
|
||||
" mode=", (transport.useClientAnimation ? "client" : "server"),
|
||||
" isM2=", transport.isM2);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy transport orientation from spline tangent.
|
||||
// Preserves the original TransportManager cross-product order for visual consistency.
|
||||
glm::quat TransportManager::orientationFromSplineTangent(const glm::vec3& tangent) {
|
||||
float tangentLenSq = glm::dot(tangent, tangent);
|
||||
if (tangentLenSq < 1e-6f) {
|
||||
return glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity
|
||||
// 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);
|
||||
}
|
||||
|
||||
glm::vec3 forward = tangent * glm::inversesqrt(tangentLenSq);
|
||||
glm::vec3 up(0.0f, 0.0f, 1.0f); // WoW Z is up
|
||||
|
||||
// If forward is nearly vertical, use different up vector
|
||||
if (std::abs(forward.z) > 0.99f) {
|
||||
up = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
glm::vec3 right = glm::normalize(glm::cross(up, forward));
|
||||
up = glm::cross(forward, right);
|
||||
|
||||
// Build rotation matrix and convert to quaternion
|
||||
glm::mat3 rotMat;
|
||||
rotMat[0] = right;
|
||||
rotMat[1] = forward;
|
||||
rotMat[2] = up;
|
||||
|
||||
return glm::quat_cast(rotMat);
|
||||
}
|
||||
|
||||
void TransportManager::updateTransformMatrices(ActiveTransport& transport) {
|
||||
|
|
@ -349,220 +262,26 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
return;
|
||||
}
|
||||
|
||||
const bool hadPrevUpdate = (transport->serverUpdateCount > 0);
|
||||
const float prevUpdateTime = transport->lastServerUpdate;
|
||||
const glm::vec3 prevPos = transport->position;
|
||||
|
||||
auto* pathEntry = pathRepo_.findPath(transport->pathId);
|
||||
const bool hasPath = (pathEntry != nullptr);
|
||||
const bool isZOnlyPath = (hasPath && pathEntry->fromDBC && pathEntry->zOnly && pathEntry->spline.durationMs() > 0);
|
||||
const bool isWorldCoordPath = (hasPath && pathEntry->worldCoords && pathEntry->spline.durationMs() > 0);
|
||||
|
||||
// Don't let (0,0,0) server updates override a TaxiPathNode world-coordinate path
|
||||
if (isWorldCoordPath && glm::dot(position, position) < 1.0f) {
|
||||
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->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Track server updates
|
||||
transport->serverUpdateCount++;
|
||||
transport->lastServerUpdate = elapsedTime_;
|
||||
// Z-only elevators and world-coordinate paths (TaxiPathNode) always stay client-driven.
|
||||
// For other DBC paths (trams, ships): only switch to server-driven mode when the server
|
||||
// sends a position that actually differs from the current position, indicating it's
|
||||
// actively streaming movement data (not just echoing the spawn position).
|
||||
if (isZOnlyPath || isWorldCoordPath) {
|
||||
transport->useClientAnimation = true;
|
||||
} else if (transport->useClientAnimation && hasPath && pathEntry->fromDBC) {
|
||||
glm::vec3 pd = position - transport->position;
|
||||
float posDeltaSq = glm::dot(pd, pd);
|
||||
if (posDeltaSq > 1.0f) {
|
||||
// Server sent a meaningfully different position — it's actively driving this transport
|
||||
transport->useClientAnimation = false;
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" switching to server-driven (posDeltaSq=", posDeltaSq, ")");
|
||||
}
|
||||
// Otherwise keep client animation (server just echoed spawn pos or sent small jitter)
|
||||
} else if (!hasPath || !pathEntry->fromDBC) {
|
||||
// No DBC path — purely server-driven
|
||||
transport->useClientAnimation = false;
|
||||
}
|
||||
transport->clientAnimationReverse = false;
|
||||
|
||||
if (!hasPath || pathEntry->spline.durationMs() == 0) {
|
||||
// No path or stationary - just set position directly
|
||||
transport->basePosition = position;
|
||||
transport->position = position;
|
||||
transport->rotation = glm::angleAxis(orientation, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
updateTransformMatrices(*transport);
|
||||
if (transport->isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
pushTransform(*transport);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-authoritative transport mode:
|
||||
// Trust explicit server world position/orientation directly for all moving transports.
|
||||
// This avoids wrong-route and direction errors when local DBC path mapping differs from server route IDs.
|
||||
transport->hasServerClock = false;
|
||||
if (transport->serverUpdateCount == 1) {
|
||||
// Seed once from first authoritative update; keep stable base for fallback phase estimation.
|
||||
// For z-only elevator paths, keep the spawn-derived basePosition (the DBC path is local offsets).
|
||||
if (!isZOnlyPath) {
|
||||
transport->basePosition = position;
|
||||
}
|
||||
}
|
||||
transport->position = position;
|
||||
transport->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
float effectiveYaw = transport->serverYaw + (transport->serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport->rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
||||
if (hadPrevUpdate) {
|
||||
const float dt = elapsedTime_ - prevUpdateTime;
|
||||
if (dt > 0.001f) {
|
||||
glm::vec3 v = (position - prevPos) / dt;
|
||||
float speedSq = glm::dot(v, v);
|
||||
constexpr float kMinAuthoritativeSpeed = 0.15f;
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
if (speedSq >= kMinAuthoritativeSpeed * kMinAuthoritativeSpeed) {
|
||||
// Auto-detect 180-degree yaw mismatch by comparing heading to movement direction.
|
||||
// Some transports appear to report yaw opposite their actual travel direction.
|
||||
glm::vec2 horizontalV(v.x, v.y);
|
||||
float hLenSq = glm::dot(horizontalV, horizontalV);
|
||||
if (hLenSq > 0.04f) {
|
||||
horizontalV *= glm::inversesqrt(hLenSq);
|
||||
glm::vec2 heading(std::cos(transport->serverYaw), std::sin(transport->serverYaw));
|
||||
float alignDot = glm::dot(heading, horizontalV);
|
||||
|
||||
if (alignDot < -0.35f) {
|
||||
transport->serverYawAlignmentScore = std::max(transport->serverYawAlignmentScore - 1, -12);
|
||||
} else if (alignDot > 0.35f) {
|
||||
transport->serverYawAlignmentScore = std::min(transport->serverYawAlignmentScore + 1, 12);
|
||||
}
|
||||
|
||||
if (!transport->serverYawFlipped180 && transport->serverYawAlignmentScore <= -4) {
|
||||
transport->serverYawFlipped180 = true;
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" enabled 180-degree yaw correction (alignScore=",
|
||||
transport->serverYawAlignmentScore, ")");
|
||||
} else if (transport->serverYawFlipped180 &&
|
||||
transport->serverYawAlignmentScore >= 4) {
|
||||
transport->serverYawFlipped180 = false;
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" disabled 180-degree yaw correction (alignScore=",
|
||||
transport->serverYawAlignmentScore, ")");
|
||||
}
|
||||
}
|
||||
|
||||
if (speedSq > kMaxSpeed * kMaxSpeed) {
|
||||
v *= (kMaxSpeed * glm::inversesqrt(speedSq));
|
||||
}
|
||||
|
||||
transport->serverLinearVelocity = v;
|
||||
transport->serverAngularVelocity = 0.0f;
|
||||
transport->hasServerVelocity = true;
|
||||
|
||||
// Re-apply potentially corrected yaw this frame after alignment check.
|
||||
effectiveYaw = transport->serverYaw + (transport->serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport->rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Seed fallback path phase from nearest waypoint to the first authoritative sample.
|
||||
if (pathEntry && pathEntry->spline.keyCount() > 0 && pathEntry->spline.durationMs() > 0) {
|
||||
glm::vec3 local = position - transport->basePosition;
|
||||
size_t bestIdx = pathEntry->spline.findNearestKey(local);
|
||||
transport->localClockMs = pathEntry->spline.keys()[bestIdx].timeMs % pathEntry->spline.durationMs();
|
||||
}
|
||||
|
||||
// Bootstrap velocity from mapped DBC path on first authoritative sample.
|
||||
// This avoids "stalled at dock" when server sends sparse transport snapshots.
|
||||
if (transport->allowBootstrapVelocity && pathEntry && pathEntry->spline.keyCount() >= 2 && pathEntry->spline.durationMs() > 0) {
|
||||
const auto& keys = pathEntry->spline.keys();
|
||||
glm::vec3 local = position - transport->basePosition;
|
||||
size_t bestIdx = pathEntry->spline.findNearestKey(local);
|
||||
|
||||
float bestDistSq = 0.0f;
|
||||
{
|
||||
glm::vec3 d = keys[bestIdx].position - local;
|
||||
bestDistSq = glm::dot(d, d);
|
||||
}
|
||||
|
||||
constexpr float kMaxBootstrapNearestDist = 80.0f;
|
||||
if (bestDistSq > (kMaxBootstrapNearestDist * kMaxBootstrapNearestDist)) {
|
||||
LOG_WARNING("Transport 0x", std::hex, guid, std::dec,
|
||||
" skipping DBC bootstrap velocity: nearest path point too far (dist=",
|
||||
std::sqrt(bestDistSq), ", path=", transport->pathId, ")");
|
||||
} else {
|
||||
size_t n = keys.size();
|
||||
uint32_t durMs = pathEntry->spline.durationMs();
|
||||
constexpr float kMinBootstrapSpeed = 0.25f;
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
|
||||
auto tryApplySegment = [&](size_t a, size_t b) {
|
||||
uint32_t t0 = keys[a].timeMs;
|
||||
uint32_t t1 = keys[b].timeMs;
|
||||
if (b == 0 && t1 <= t0 && durMs > 0) {
|
||||
t1 = durMs;
|
||||
}
|
||||
if (t1 <= t0) return;
|
||||
glm::vec3 seg = keys[b].position - keys[a].position;
|
||||
float dtSeg = static_cast<float>(t1 - t0) / 1000.0f;
|
||||
if (dtSeg <= 0.001f) return;
|
||||
glm::vec3 v = seg / dtSeg;
|
||||
float speedSq = glm::dot(v, v);
|
||||
if (speedSq < kMinBootstrapSpeed * kMinBootstrapSpeed) return;
|
||||
if (speedSq > kMaxSpeed * kMaxSpeed) {
|
||||
v *= (kMaxSpeed * glm::inversesqrt(speedSq));
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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) {
|
||||
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, ")");
|
||||
} else {
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" skipped DBC bootstrap velocity (segment too short/static)");
|
||||
}
|
||||
}
|
||||
} else if (!transport->allowBootstrapVelocity) {
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" DBC bootstrap velocity disabled for this transport");
|
||||
}
|
||||
}
|
||||
// Delegate clock sync, yaw correction, and velocity bootstrap to ClockSync.
|
||||
clockSync_.processServerUpdate(*transport, pathEntry, position, orientation, elapsedTime_);
|
||||
|
||||
updateTransformMatrices(*transport);
|
||||
if (transport->isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
return;
|
||||
pushTransform(*transport);
|
||||
}
|
||||
|
||||
bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) {
|
||||
|
|
@ -609,9 +328,7 @@ bool TransportManager::assignTaxiPathToTransport(uint32_t entry, uint32_t taxiPa
|
|||
}
|
||||
|
||||
updateTransformMatrices(transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
pushTransform(transport);
|
||||
|
||||
LOG_INFO("Assigned TaxiPathNode path to transport 0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " taxiPathId=", taxiPathId,
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ add_executable(test_entity
|
|||
test_entity.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/game/entity.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/math/spline.cpp
|
||||
)
|
||||
target_include_directories(test_entity PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_entity SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
|
|
@ -236,6 +237,20 @@ endif()
|
|||
add_test(NAME indoor_shadows COMMAND test_indoor_shadows)
|
||||
register_test_target(test_indoor_shadows)
|
||||
|
||||
# ── test_transport_components ────────────────────────────────
|
||||
add_executable(test_transport_components
|
||||
test_transport_components.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/game/transport_clock_sync.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/transport_animator.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/math/spline.cpp
|
||||
)
|
||||
target_include_directories(test_transport_components PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_transport_components SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_transport_components PRIVATE catch2_main)
|
||||
add_test(NAME transport_components COMMAND test_transport_components)
|
||||
register_test_target(test_transport_components)
|
||||
|
||||
# ── ASAN / UBSan for test targets ────────────────────────────
|
||||
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
|
||||
foreach(_t IN LISTS ALL_TEST_TARGETS)
|
||||
|
|
|
|||
209
tests/test_transport_components.cpp
Normal file
209
tests/test_transport_components.cpp
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
// tests/test_transport_components.cpp
|
||||
// Unit tests for TransportClockSync and TransportAnimator (Phase 3 extractions).
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "game/transport_clock_sync.hpp"
|
||||
#include "game/transport_animator.hpp"
|
||||
#include "game/transport_manager.hpp"
|
||||
#include "game/transport_path_repository.hpp"
|
||||
#include "math/spline.hpp"
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <cmath>
|
||||
|
||||
using namespace wowee::game;
|
||||
using namespace wowee::math;
|
||||
|
||||
// ── Helper: build a simple circular path ──────────────────────────
|
||||
static PathEntry makeCirclePath() {
|
||||
// Circle-ish path with 4 points, 4000ms duration
|
||||
std::vector<SplineKey> keys = {
|
||||
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
||||
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
||||
{2000, glm::vec3(10.0f, 10.0f, 0.0f)},
|
||||
{3000, glm::vec3(0.0f, 10.0f, 0.0f)},
|
||||
{4000, glm::vec3(0.0f, 0.0f, 0.0f)},
|
||||
};
|
||||
CatmullRomSpline spline(std::move(keys), /*timeClosed=*/true);
|
||||
return PathEntry(std::move(spline), /*pathId=*/100, /*zOnly=*/false, /*fromDBC=*/true, /*worldCoords=*/false);
|
||||
}
|
||||
|
||||
// ── Helper: create a fresh ActiveTransport ────────────────────────
|
||||
static ActiveTransport makeTransport(uint64_t guid = 1, uint32_t pathId = 100) {
|
||||
ActiveTransport t{};
|
||||
t.guid = guid;
|
||||
t.pathId = pathId;
|
||||
t.basePosition = glm::vec3(100.0f, 200.0f, 0.0f);
|
||||
t.position = glm::vec3(100.0f, 200.0f, 0.0f);
|
||||
t.rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
t.playerOnBoard = false;
|
||||
t.playerLocalOffset = glm::vec3(0);
|
||||
t.hasDeckBounds = false;
|
||||
t.localClockMs = 0;
|
||||
t.hasServerClock = false;
|
||||
t.serverClockOffsetMs = 0;
|
||||
t.useClientAnimation = true;
|
||||
t.clientAnimationReverse = false;
|
||||
t.serverYaw = 0.0f;
|
||||
t.hasServerYaw = false;
|
||||
t.serverYawFlipped180 = false;
|
||||
t.serverYawAlignmentScore = 0;
|
||||
t.lastServerUpdate = 0.0;
|
||||
t.serverUpdateCount = 0;
|
||||
t.serverLinearVelocity = glm::vec3(0);
|
||||
t.serverAngularVelocity = 0.0f;
|
||||
t.hasServerVelocity = false;
|
||||
t.allowBootstrapVelocity = true;
|
||||
t.isM2 = false;
|
||||
return t;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// TransportClockSync tests
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST_CASE("ClockSync: client animation advances localClockMs", "[transport_clock_sync]") {
|
||||
TransportClockSync sync;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.useClientAnimation = true;
|
||||
t.hasServerClock = false;
|
||||
|
||||
uint32_t pathTimeMs = 0;
|
||||
bool result = sync.computePathTime(t, path.spline, 1.0, 0.016f, pathTimeMs);
|
||||
REQUIRE(result);
|
||||
REQUIRE(t.localClockMs > 0); // Should have advanced
|
||||
REQUIRE(pathTimeMs == t.localClockMs % path.spline.durationMs());
|
||||
}
|
||||
|
||||
TEST_CASE("ClockSync: server clock mode wraps correctly", "[transport_clock_sync]") {
|
||||
TransportClockSync sync;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.hasServerClock = true;
|
||||
t.serverClockOffsetMs = 500; // Server is 500ms ahead
|
||||
|
||||
uint32_t pathTimeMs = 0;
|
||||
double elapsedTime = 3.7; // 3700ms local → 4200ms server → 200ms wrapped (dur=4000)
|
||||
bool result = sync.computePathTime(t, path.spline, elapsedTime, 0.016f, pathTimeMs);
|
||||
REQUIRE(result);
|
||||
REQUIRE(pathTimeMs == 200);
|
||||
}
|
||||
|
||||
TEST_CASE("ClockSync: strict server mode returns false", "[transport_clock_sync]") {
|
||||
TransportClockSync sync;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.useClientAnimation = false;
|
||||
t.hasServerClock = false;
|
||||
|
||||
uint32_t pathTimeMs = 0;
|
||||
bool result = sync.computePathTime(t, path.spline, 1.0, 0.016f, pathTimeMs);
|
||||
REQUIRE_FALSE(result);
|
||||
}
|
||||
|
||||
TEST_CASE("ClockSync: reverse client animation decrements", "[transport_clock_sync]") {
|
||||
TransportClockSync sync;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.useClientAnimation = true;
|
||||
t.clientAnimationReverse = true;
|
||||
t.localClockMs = 2000;
|
||||
|
||||
uint32_t pathTimeMs = 0;
|
||||
bool result = sync.computePathTime(t, path.spline, 1.0, 0.5f, pathTimeMs);
|
||||
REQUIRE(result);
|
||||
// localClockMs should have decreased by ~500ms
|
||||
REQUIRE(t.localClockMs < 2000);
|
||||
}
|
||||
|
||||
TEST_CASE("ClockSync: processServerUpdate sets yaw and rotation", "[transport_clock_sync]") {
|
||||
TransportClockSync sync;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
|
||||
glm::vec3 pos(105.0f, 205.0f, 1.0f);
|
||||
float yaw = 1.5f;
|
||||
sync.processServerUpdate(t, &path, pos, yaw, 10.0);
|
||||
|
||||
REQUIRE(t.serverUpdateCount == 1);
|
||||
REQUIRE(t.hasServerYaw);
|
||||
REQUIRE(t.serverYaw == Catch::Approx(1.5f));
|
||||
REQUIRE(t.position == pos);
|
||||
}
|
||||
|
||||
TEST_CASE("ClockSync: yaw flip detection after repeated misaligned updates", "[transport_clock_sync]") {
|
||||
TransportClockSync sync;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.useClientAnimation = false;
|
||||
|
||||
// Simulate transport moving east (+X) but reporting yaw pointing west (pi)
|
||||
float westYaw = glm::pi<float>();
|
||||
glm::vec3 pos(100.0f, 200.0f, 0.0f);
|
||||
sync.processServerUpdate(t, &path, pos, westYaw, 1.0);
|
||||
|
||||
// Send several updates moving east with west-facing yaw
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
pos.x += 5.0f;
|
||||
sync.processServerUpdate(t, &path, pos, westYaw, 1.0 + i * 0.5);
|
||||
}
|
||||
|
||||
// After enough misaligned updates, should have flipped
|
||||
REQUIRE(t.serverYawFlipped180);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// TransportAnimator tests
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST_CASE("Animator: evaluateAndApply updates position from spline", "[transport_animator]") {
|
||||
TransportAnimator animator;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.hasServerYaw = false;
|
||||
|
||||
animator.evaluateAndApply(t, path, 0);
|
||||
// At t=0, path offset is (0,0,0), so pos = base + (0,0,0) = (100,200,0)
|
||||
REQUIRE(t.position.x == Catch::Approx(100.0f));
|
||||
REQUIRE(t.position.y == Catch::Approx(200.0f));
|
||||
|
||||
animator.evaluateAndApply(t, path, 1000);
|
||||
// At t=1000, path offset is (10,0,0), so pos = base + (10,0,0) = (110,200,0)
|
||||
REQUIRE(t.position.x == Catch::Approx(110.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("Animator: uses server yaw when available", "[transport_animator]") {
|
||||
TransportAnimator animator;
|
||||
auto path = makeCirclePath();
|
||||
auto t = makeTransport();
|
||||
t.hasServerYaw = true;
|
||||
t.serverYaw = 1.0f;
|
||||
t.serverYawFlipped180 = false;
|
||||
|
||||
animator.evaluateAndApply(t, path, 500);
|
||||
// Rotation should be based on serverYaw=1.0, not spline tangent
|
||||
float expectedYaw = 1.0f;
|
||||
glm::quat expected = glm::angleAxis(expectedYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
REQUIRE(t.rotation.w == Catch::Approx(expected.w).margin(0.01f));
|
||||
REQUIRE(t.rotation.z == Catch::Approx(expected.z).margin(0.01f));
|
||||
}
|
||||
|
||||
TEST_CASE("Animator: Z clamping on non-world-coord client anim", "[transport_animator]") {
|
||||
TransportAnimator animator;
|
||||
|
||||
// Build a path with a deep negative Z offset
|
||||
std::vector<SplineKey> keys = {
|
||||
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
||||
{1000, glm::vec3(5.0f, 0.0f, -50.0f)}, // Deep negative Z
|
||||
{2000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
||||
};
|
||||
CatmullRomSpline spline(std::move(keys), false);
|
||||
PathEntry path(std::move(spline), 200, false, true, false);
|
||||
|
||||
auto t = makeTransport();
|
||||
t.useClientAnimation = true;
|
||||
t.serverUpdateCount = 0; // <= 1, so Z clamping applies
|
||||
|
||||
animator.evaluateAndApply(t, path, 1000);
|
||||
// Z should be clamped to >= -2.0 (kMinFallbackZOffset)
|
||||
REQUIRE(t.position.z >= (t.basePosition.z - 2.0f));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue