mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Stabilize transports and correct minimap orientation
This commit is contained in:
parent
2bd259c0a8
commit
5dae994830
6 changed files with 235 additions and 203 deletions
|
|
@ -55,11 +55,17 @@ struct ActiveTransport {
|
|||
bool hasServerClock; // Whether we've synced with server time
|
||||
int32_t serverClockOffsetMs; // Offset: serverClock - localNow
|
||||
bool useClientAnimation; // Use client-side path animation
|
||||
bool clientAnimationReverse; // Run client animation in reverse along the selected path
|
||||
float serverYaw; // Server-authoritative yaw (radians)
|
||||
bool hasServerYaw; // Whether we've received server yaw
|
||||
|
||||
float lastServerUpdate; // Time of last server movement update
|
||||
int serverUpdateCount; // Number of server updates received
|
||||
|
||||
// Dead-reckoning from latest authoritative updates (used only when updates are sparse).
|
||||
glm::vec3 serverLinearVelocity;
|
||||
float serverAngularVelocity;
|
||||
bool hasServerVelocity;
|
||||
};
|
||||
|
||||
class TransportManager {
|
||||
|
|
@ -85,6 +91,8 @@ public:
|
|||
|
||||
// Check if a path exists for a given GameObject entry
|
||||
bool hasPathForEntry(uint32_t entry) const;
|
||||
// Check if a path has meaningful XY travel (used to reject near-stationary false positives).
|
||||
bool hasUsableMovingPathForEntry(uint32_t entry, float minXYRange = 1.0f) const;
|
||||
|
||||
// Infer a real moving DBC path by spawn position (for servers whose transport entry IDs
|
||||
// don't map 1:1 to TransportAnimation.dbc entry IDs).
|
||||
|
|
|
|||
|
|
@ -890,17 +890,28 @@ void Application::setupUICallbacks() {
|
|||
|
||||
// Check if we have a real path from TransportAnimation.dbc (indexed by entry).
|
||||
// AzerothCore transport entries are not always 1:1 with DBC path ids.
|
||||
if (!transportManager->hasPathForEntry(entry)) {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Using remapped fallback transport path ", pathId,
|
||||
" for entry ", entry, " displayId=", displayId);
|
||||
const bool shipOrZeppelinDisplay =
|
||||
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
|
||||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
|
||||
displayId == 807 || displayId == 808 || displayId == 455 || displayId == 462);
|
||||
bool hasUsablePath = transportManager->hasPathForEntry(entry);
|
||||
if (shipOrZeppelinDisplay) {
|
||||
// For true transports, reject tiny XY tracks that effectively look stationary.
|
||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
||||
}
|
||||
|
||||
if (!hasUsablePath) {
|
||||
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_INFO("Using inferred transport path ", pathId, " for entry ", entry);
|
||||
} else {
|
||||
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_INFO("Using inferred transport path ", pathId, " for entry ", entry);
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Using remapped fallback transport path ", pathId,
|
||||
" for entry ", entry, " displayId=", displayId,
|
||||
" (usableEntryPath=", transportManager->hasPathForEntry(entry), ")");
|
||||
} else {
|
||||
LOG_WARNING("No TransportAnimation.dbc path for entry ", entry,
|
||||
" - transport will be stationary");
|
||||
|
|
@ -957,20 +968,29 @@ void Application::setupUICallbacks() {
|
|||
// Coordinates are already canonical (converted in game_handler.cpp)
|
||||
glm::vec3 canonicalSpawnPos(x, y, z);
|
||||
|
||||
// Check if we have a real path, otherwise remap/infer/fall back to stationary.
|
||||
if (!transportManager->hasPathForEntry(entry)) {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Auto-spawned transport with remapped fallback path: entry=", entry,
|
||||
" remappedPath=", pathId, " displayId=", displayId,
|
||||
// Check if we have a real usable path, otherwise remap/infer/fall back to stationary.
|
||||
const bool shipOrZeppelinDisplay =
|
||||
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
|
||||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
|
||||
displayId == 807 || displayId == 808 || displayId == 455 || displayId == 462);
|
||||
bool hasUsablePath = transportManager->hasPathForEntry(entry);
|
||||
if (shipOrZeppelinDisplay) {
|
||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
||||
}
|
||||
|
||||
if (!hasUsablePath) {
|
||||
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_INFO("Auto-spawned transport with inferred path: entry=", entry,
|
||||
" inferredPath=", pathId, " displayId=", displayId,
|
||||
" wmoInstance=", wmoInstanceId);
|
||||
} else {
|
||||
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_INFO("Auto-spawned transport with inferred path: entry=", entry,
|
||||
" inferredPath=", pathId, " displayId=", displayId,
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Auto-spawned transport with remapped fallback path: entry=", entry,
|
||||
" remappedPath=", pathId, " displayId=", displayId,
|
||||
" wmoInstance=", wmoInstanceId);
|
||||
} else {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
|
|
|
|||
|
|
@ -1853,6 +1853,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
// Process out-of-range objects first
|
||||
for (uint64_t guid : data.outOfRangeGuids) {
|
||||
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;
|
||||
}
|
||||
|
||||
LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec);
|
||||
// Trigger despawn callbacks before removing entity
|
||||
auto entity = entityManager.getEntity(guid);
|
||||
|
|
@ -1934,11 +1940,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
|
||||
// Refresh parent transport transform from this packet stream.
|
||||
if (transportMoveCallback_) {
|
||||
transportMoveCallback_(block.transportGuid, pos.x, pos.y, pos.z, block.orientation);
|
||||
}
|
||||
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
|
|
@ -2428,10 +2429,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
glm::vec3 localOffset = core::coords::serverToCanonical(
|
||||
glm::vec3(block.transportX, block.transportY, block.transportZ));
|
||||
|
||||
if (transportMoveCallback_) {
|
||||
transportMoveCallback_(block.transportGuid, pos.x, pos.y, pos.z, block.orientation);
|
||||
}
|
||||
|
||||
if (transportManager_ && transportManager_->getTransport(block.transportGuid)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation());
|
||||
|
|
@ -2538,6 +2535,10 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
|
|||
|
||||
// Remove entity
|
||||
if (entityManager.hasEntity(data.guid)) {
|
||||
if (transportGuids_.count(data.guid) > 0) {
|
||||
LOG_INFO("Ignoring destroy for transport entity: 0x", std::hex, data.guid, std::dec);
|
||||
return;
|
||||
}
|
||||
entityManager.removeEntity(data.guid);
|
||||
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
|
||||
" (", (data.isDeath ? "death" : "despawn"), ")");
|
||||
|
|
|
|||
|
|
@ -76,10 +76,14 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.serverClockOffsetMs = 0;
|
||||
// Server-authoritative movement only - no client-side animation
|
||||
transport.useClientAnimation = false;
|
||||
transport.clientAnimationReverse = false;
|
||||
transport.serverYaw = 0.0f;
|
||||
transport.hasServerYaw = false;
|
||||
transport.lastServerUpdate = 0.0f;
|
||||
transport.serverUpdateCount = 0;
|
||||
transport.serverLinearVelocity = glm::vec3(0.0f);
|
||||
transport.serverAngularVelocity = 0.0f;
|
||||
transport.hasServerVelocity = false;
|
||||
|
||||
updateTransformMatrices(transport);
|
||||
|
||||
|
|
@ -235,28 +239,116 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
pathTimeMs = (uint32_t)wrapped;
|
||||
} else if (transport.useClientAnimation) {
|
||||
// Pure local clock (no server sync yet, client-driven)
|
||||
transport.localClockMs += (uint32_t)(deltaTime * 1000.0f);
|
||||
uint32_t dtMs = static_cast<uint32_t>(deltaTime * 1000.0f);
|
||||
if (!transport.clientAnimationReverse) {
|
||||
transport.localClockMs += dtMs;
|
||||
} else {
|
||||
if (dtMs > path.durationMs) {
|
||||
dtMs %= path.durationMs;
|
||||
}
|
||||
if (transport.localClockMs >= dtMs) {
|
||||
transport.localClockMs -= dtMs;
|
||||
} else {
|
||||
transport.localClockMs = path.durationMs - (dtMs - transport.localClockMs);
|
||||
}
|
||||
}
|
||||
pathTimeMs = transport.localClockMs % path.durationMs;
|
||||
} else {
|
||||
// Server-driven but no clock yet. If updates never arrive, fall back to local animation.
|
||||
constexpr float kMissingUpdateFallbackSec = 2.5f;
|
||||
if ((elapsedTime_ - transport.lastServerUpdate) >= kMissingUpdateFallbackSec) {
|
||||
transport.useClientAnimation = true;
|
||||
transport.localClockMs = 0;
|
||||
pathTimeMs = 0;
|
||||
LOG_WARNING("TransportManager: No server movement updates for transport 0x", std::hex, transport.guid, std::dec,
|
||||
" after ", kMissingUpdateFallbackSec, "s - enabling client fallback animation");
|
||||
} else {
|
||||
updateTransformMatrices(transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
// Server-driven transport without clock sync.
|
||||
// Do not auto-fallback to local DBC paths; remapped paths can be wrong and cause
|
||||
// fast sideways movement, diving below water, or despawn-like behavior.
|
||||
// Instead, briefly dead-reckon from recent authoritative velocity to avoid visual stutter
|
||||
// when update bursts are sparse.
|
||||
constexpr float kMaxExtrapolationSec = 8.0f;
|
||||
const float ageSec = elapsedTime_ - transport.lastServerUpdate;
|
||||
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);
|
||||
} else if (transport.serverUpdateCount <= 1 &&
|
||||
ageSec >= 1.0f &&
|
||||
path.fromDBC && !path.zOnly && path.durationMs > 0 && path.points.size() > 1 &&
|
||||
((transport.guid & 0xFFF0000000000000ULL) == 0x1FC0000000000000ULL)) {
|
||||
// Spawn-only fallback: only for world transport GUIDs (0x1fc...), not all transport-like objects.
|
||||
glm::vec3 localTarget = transport.position - transport.basePosition;
|
||||
uint32_t bestTimeMs = 0;
|
||||
float bestScore = FLT_MAX;
|
||||
float bestD2 = FLT_MAX;
|
||||
constexpr uint32_t samples = 600;
|
||||
for (uint32_t i = 0; i < samples; ++i) {
|
||||
uint32_t t = static_cast<uint32_t>((static_cast<uint64_t>(i) * path.durationMs) / samples);
|
||||
glm::vec3 off = evalTimedCatmullRom(path, t);
|
||||
glm::vec3 d = off - localTarget;
|
||||
float d2 = glm::dot(d, d);
|
||||
|
||||
float score = d2;
|
||||
if (transport.hasServerYaw) {
|
||||
constexpr uint32_t kHeadingDtMs = 250;
|
||||
uint32_t tNext = (t + kHeadingDtMs) % path.durationMs;
|
||||
glm::vec3 offNext = evalTimedCatmullRom(path, tNext);
|
||||
glm::vec3 tangent = offNext - off;
|
||||
if (glm::length2(tangent) > 1e-6f) {
|
||||
float yaw = std::atan2(tangent.y, tangent.x);
|
||||
float dyaw = yaw - transport.serverYaw;
|
||||
while (dyaw > glm::pi<float>()) dyaw -= glm::two_pi<float>();
|
||||
while (dyaw < -glm::pi<float>()) dyaw += glm::two_pi<float>();
|
||||
constexpr float kHeadingWeight = 60.0f;
|
||||
score += (kHeadingWeight * std::abs(dyaw)) * (kHeadingWeight * std::abs(dyaw));
|
||||
}
|
||||
}
|
||||
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestD2 = d2;
|
||||
bestTimeMs = t;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float kMaxPhaseDrift = 120.0f;
|
||||
if (bestD2 <= (kMaxPhaseDrift * kMaxPhaseDrift)) {
|
||||
bool reverse = false;
|
||||
if (transport.hasServerYaw) {
|
||||
constexpr uint32_t kYawDtMs = 250;
|
||||
uint32_t tNext = (bestTimeMs + kYawDtMs) % path.durationMs;
|
||||
glm::vec3 p0 = evalTimedCatmullRom(path, bestTimeMs);
|
||||
glm::vec3 p1 = evalTimedCatmullRom(path, tNext);
|
||||
glm::vec3 d = p1 - p0;
|
||||
if (glm::length2(d) > 1e-6f) {
|
||||
float yawFwd = std::atan2(d.y, d.x);
|
||||
float yawRev = yawFwd + glm::pi<float>();
|
||||
auto angleDiff = [](float a, float b) -> float {
|
||||
float d = a - b;
|
||||
while (d > glm::pi<float>()) d -= glm::two_pi<float>();
|
||||
while (d < -glm::pi<float>()) d += glm::two_pi<float>();
|
||||
return std::abs(d);
|
||||
};
|
||||
reverse = angleDiff(yawRev, transport.serverYaw) < angleDiff(yawFwd, transport.serverYaw);
|
||||
}
|
||||
}
|
||||
|
||||
transport.useClientAnimation = true;
|
||||
transport.localClockMs = bestTimeMs;
|
||||
transport.clientAnimationReverse = reverse;
|
||||
LOG_WARNING("TransportManager: No follow-up server updates for world transport 0x", std::hex, transport.guid, std::dec,
|
||||
" (", ageSec, "s since spawn); enabling guarded fallback at t=", bestTimeMs,
|
||||
"ms (phaseDrift=", std::sqrt(bestD2), ", reverse=", reverse, ")");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
updateTransformMatrices(transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate position from time (path is local offsets, add base position)
|
||||
glm::vec3 pathOffset = evalTimedCatmullRom(path, 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.
|
||||
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
|
||||
}
|
||||
transport.position = transport.basePosition + pathOffset;
|
||||
|
||||
// Use server yaw if available (authoritative), otherwise compute from tangent
|
||||
|
|
@ -503,10 +595,15 @@ 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;
|
||||
|
||||
// Track server updates
|
||||
transport->serverUpdateCount++;
|
||||
transport->lastServerUpdate = elapsedTime_;
|
||||
transport->useClientAnimation = false; // Server updates take precedence
|
||||
transport->clientAnimationReverse = false;
|
||||
|
||||
auto pathIt = paths_.find(transport->pathId);
|
||||
if (pathIt == paths_.end() || pathIt->second.durationMs == 0) {
|
||||
|
|
@ -521,162 +618,41 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
return;
|
||||
}
|
||||
|
||||
const auto& path = pathIt->second;
|
||||
|
||||
// Z-only paths (elevator/bobbing): server is authoritative, no projection needed
|
||||
if (path.zOnly) {
|
||||
transport->position = position;
|
||||
transport->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
transport->useClientAnimation = false; // Server-driven
|
||||
|
||||
updateTransformMatrices(*transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
|
||||
LOG_INFO("TransportManager: Z-only transport 0x", std::hex, guid, std::dec,
|
||||
" updated from server: pos=(", position.x, ", ", position.y, ", ", position.z, ")");
|
||||
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;
|
||||
transport->useClientAnimation = false;
|
||||
if (transport->serverUpdateCount == 1) {
|
||||
// Seed once from first authoritative update; keep stable base for fallback phase estimation.
|
||||
transport->basePosition = position;
|
||||
}
|
||||
|
||||
// Seed basePosition from t=0 assumption before first search
|
||||
// (t=0 corresponds to spawn point / first path point)
|
||||
if (!transport->hasServerClock) {
|
||||
glm::vec3 offset0 = evalTimedCatmullRom(path, 0);
|
||||
transport->basePosition = position - offset0;
|
||||
}
|
||||
|
||||
// Estimate server's path time by projecting position onto path
|
||||
// Path positions are local offsets, server position is world position
|
||||
// basePosition = serverWorldPos - pathLocalOffset
|
||||
|
||||
uint32_t bestTimeMs = 0;
|
||||
float bestD2 = FLT_MAX;
|
||||
glm::vec3 bestPathOffset(0.0f);
|
||||
|
||||
// After initial sync, search only in small window around predicted time
|
||||
bool hasInitialSync = transport->hasServerClock;
|
||||
uint32_t nowMs = (uint32_t)(elapsedTime_ * 1000.0f);
|
||||
uint32_t predictedTimeMs = 0;
|
||||
if (hasInitialSync) {
|
||||
// Predict where server should be based on last clock offset
|
||||
int64_t serverTimeMs = (int64_t)nowMs + transport->serverClockOffsetMs;
|
||||
int64_t mod = (int64_t)path.durationMs;
|
||||
int64_t wrapped = serverTimeMs % mod;
|
||||
if (wrapped < 0) wrapped += mod;
|
||||
predictedTimeMs = (uint32_t)wrapped;
|
||||
}
|
||||
|
||||
uint32_t searchStart = 0;
|
||||
uint32_t searchEnd = path.durationMs;
|
||||
uint32_t sampleCount = 1000; // Dense sampling for accuracy
|
||||
|
||||
if (hasInitialSync) {
|
||||
// Search in ±5 second window around predicted time
|
||||
uint32_t windowMs = 5000;
|
||||
searchStart = (predictedTimeMs > windowMs) ? (predictedTimeMs - windowMs) : 0;
|
||||
searchEnd = glm::min(predictedTimeMs + windowMs, path.durationMs);
|
||||
sampleCount = 200; // Fewer samples needed in small window
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < sampleCount; i++) {
|
||||
// Map i to [searchStart, searchEnd)
|
||||
uint32_t testTimeMs = searchStart + (uint32_t)((uint64_t)i * (searchEnd - searchStart) / sampleCount);
|
||||
glm::vec3 testPathOffset = evalTimedCatmullRom(path, testTimeMs);
|
||||
glm::vec3 testWorldPos = transport->basePosition + testPathOffset; // Convert local → world
|
||||
glm::vec3 diff = testWorldPos - position;
|
||||
float d2 = glm::dot(diff, diff); // distance² (cheaper, no sqrt)
|
||||
if (d2 < bestD2) {
|
||||
bestD2 = d2;
|
||||
bestTimeMs = testTimeMs;
|
||||
bestPathOffset = testPathOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// Refine with finer sampling around best match
|
||||
uint32_t refineSampleCount = 50;
|
||||
uint32_t refineWindow = glm::max(1u, (searchEnd - searchStart) / sampleCount); // Clamp to prevent zero
|
||||
uint32_t refineStart = (bestTimeMs > refineWindow) ? (bestTimeMs - refineWindow) : 0;
|
||||
uint32_t refineEnd = glm::min(bestTimeMs + refineWindow, path.durationMs);
|
||||
uint32_t refineInterval = (refineEnd > refineStart) ? ((refineEnd - refineStart) / refineSampleCount) : 1;
|
||||
if (refineInterval > 0) {
|
||||
for (uint32_t i = 0; i < refineSampleCount; i++) {
|
||||
uint32_t testTimeMs = refineStart + i * refineInterval;
|
||||
glm::vec3 testPathOffset = evalTimedCatmullRom(path, testTimeMs); // local offset
|
||||
glm::vec3 testWorldPos = transport->basePosition + testPathOffset; // Convert local → world
|
||||
glm::vec3 diff = testWorldPos - position; // Compare world to world
|
||||
float d2 = glm::dot(diff, diff);
|
||||
if (d2 < bestD2) {
|
||||
bestD2 = d2;
|
||||
bestTimeMs = testTimeMs;
|
||||
bestPathOffset = testPathOffset; // Update best offset when improving match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float bestDistance = std::sqrt(bestD2);
|
||||
|
||||
// Infer base position: serverWorldPos = basePos + pathOffset
|
||||
// So: basePos = serverWorldPos - pathOffset
|
||||
glm::vec3 inferredBasePos = position - bestPathOffset;
|
||||
|
||||
// Compute server clock offset with wrap-aware smoothing
|
||||
int32_t newOffset = (int32_t)bestTimeMs - (int32_t)nowMs;
|
||||
|
||||
if (!transport->hasServerClock) {
|
||||
// First sync: accept immediately and set base position
|
||||
transport->basePosition = inferredBasePos;
|
||||
transport->serverClockOffsetMs = newOffset;
|
||||
transport->hasServerClock = true;
|
||||
LOG_INFO("TransportManager: Initial server clock sync for transport 0x", std::hex, guid, std::dec,
|
||||
" serverTime=", bestTimeMs, "ms / ", path.durationMs, "ms",
|
||||
" drift=", bestDistance, " units",
|
||||
" basePos=(", inferredBasePos.x, ", ", inferredBasePos.y, ", ", inferredBasePos.z, ")",
|
||||
" offset=", newOffset, "ms");
|
||||
} else {
|
||||
// Subsequent syncs: wrap-aware smoothing to avoid phase jumps
|
||||
int32_t oldOffset = transport->serverClockOffsetMs;
|
||||
int32_t delta = newOffset - oldOffset;
|
||||
int32_t mod = (int32_t)path.durationMs;
|
||||
|
||||
// Wrap delta to shortest path: [-mod/2, mod/2]
|
||||
if (delta > mod / 2) delta -= mod;
|
||||
if (delta < -mod / 2) delta += mod;
|
||||
|
||||
// Smooth delta, not absolute offset
|
||||
transport->serverClockOffsetMs = oldOffset + (int32_t)(0.1f * delta);
|
||||
|
||||
// Only update basePosition if projection is accurate (< 5 units drift)
|
||||
// This prevents "swim" from projection noise near ambiguous geometry
|
||||
if (bestDistance < 5.0f) {
|
||||
transport->basePosition = glm::mix(transport->basePosition, inferredBasePos, 0.1f);
|
||||
LOG_INFO("TransportManager: Server clock correction for transport 0x", std::hex, guid, std::dec,
|
||||
" drift=", bestDistance, " units (updated base)",
|
||||
" oldOffset=", oldOffset, "ms → newOffset=", transport->serverClockOffsetMs, "ms",
|
||||
" (delta=", delta, "ms, smoothed by 0.1)");
|
||||
} else {
|
||||
LOG_INFO("TransportManager: Server clock correction for transport 0x", std::hex, guid, std::dec,
|
||||
" drift=", bestDistance, " units (base unchanged, clock only)",
|
||||
" oldOffset=", oldOffset, "ms → newOffset=", transport->serverClockOffsetMs, "ms",
|
||||
" (delta=", delta, "ms, smoothed by 0.1)");
|
||||
}
|
||||
}
|
||||
|
||||
// Update position immediately from synced clock
|
||||
glm::vec3 pathOffset = evalTimedCatmullRom(path, bestTimeMs);
|
||||
transport->position = transport->basePosition + pathOffset;
|
||||
|
||||
// Store server's authoritative yaw (orientation is in radians around Z axis)
|
||||
transport->position = position;
|
||||
transport->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
transport->rotation = glm::angleAxis(transport->serverYaw, 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;
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
|
||||
updateTransformMatrices(*transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) {
|
||||
|
|
@ -873,6 +849,30 @@ bool TransportManager::hasPathForEntry(uint32_t entry) const {
|
|||
return it != paths_.end() && it->second.fromDBC;
|
||||
}
|
||||
|
||||
bool TransportManager::hasUsableMovingPathForEntry(uint32_t entry, float minXYRange) const {
|
||||
auto it = paths_.find(entry);
|
||||
if (it == paths_.end()) return false;
|
||||
|
||||
const auto& path = it->second;
|
||||
if (!path.fromDBC || path.points.size() < 2 || path.durationMs == 0 || path.zOnly) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float minX = path.points.front().pos.x;
|
||||
float maxX = minX;
|
||||
float minY = path.points.front().pos.y;
|
||||
float maxY = minY;
|
||||
for (const auto& p : path.points) {
|
||||
minX = std::min(minX, p.pos.x);
|
||||
maxX = std::max(maxX, p.pos.x);
|
||||
minY = std::min(minY, p.pos.y);
|
||||
maxY = std::max(maxY, p.pos.y);
|
||||
}
|
||||
|
||||
float rangeXY = std::max(maxX - minX, maxY - minY);
|
||||
return rangeXY >= minXYRange;
|
||||
}
|
||||
|
||||
uint32_t TransportManager::inferMovingPathForSpawn(const glm::vec3& spawnWorldPos, float maxDistance) const {
|
||||
float bestD2 = maxDistance * maxDistance;
|
||||
uint32_t bestPathId = 0;
|
||||
|
|
|
|||
|
|
@ -173,16 +173,15 @@ bool Minimap::initialize(int size) {
|
|||
if (maxDist > 0.5) discard;
|
||||
|
||||
// Rotate screen coords → composite UV offset
|
||||
// Composite: U increases east, V increases south
|
||||
// Composite: U increases east, V increases north
|
||||
// Screen: +X=right, +Y=up
|
||||
// The -cos(a) term in dV inherently flips V (screen up → composite north)
|
||||
float c = cos(uRotation);
|
||||
float s = sin(uRotation);
|
||||
float scale = uZoomRadius * 2.0;
|
||||
|
||||
vec2 offset = vec2(
|
||||
centered.x * c + centered.y * s,
|
||||
centered.x * s - centered.y * c
|
||||
-centered.x * s + centered.y * c
|
||||
) * scale;
|
||||
|
||||
vec2 uv = uPlayerUV + offset;
|
||||
|
|
@ -194,7 +193,7 @@ bool Minimap::initialize(int size) {
|
|||
}
|
||||
|
||||
// Player arrow at center (always points up = forward)
|
||||
vec2 ap = rot2(centered, -uArrowRotation);
|
||||
vec2 ap = rot2(centered, -(uArrowRotation + 3.14159265));
|
||||
vec2 tip = vec2(0.0, 0.035);
|
||||
vec2 lt = vec2(-0.018, -0.016);
|
||||
vec2 rt = vec2(0.018, -0.016);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) {
|
||||
if (auto* minimap = renderer->getMinimap()) {
|
||||
minimap->setRotateWithCamera(minimapRotate_);
|
||||
minimapRotate_ = false;
|
||||
pendingMinimapRotate = false;
|
||||
minimap->setRotateWithCamera(false);
|
||||
minimap->setSquareShape(minimapSquare_);
|
||||
minimapSettingsApplied_ = true;
|
||||
}
|
||||
|
|
@ -4651,10 +4653,12 @@ void GameScreen::renderSettingsWindow() {
|
|||
saveSettings();
|
||||
}
|
||||
if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) {
|
||||
minimapRotate_ = pendingMinimapRotate;
|
||||
// Force north-up minimap.
|
||||
minimapRotate_ = false;
|
||||
pendingMinimapRotate = false;
|
||||
if (renderer) {
|
||||
if (auto* minimap = renderer->getMinimap()) {
|
||||
minimap->setRotateWithCamera(minimapRotate_);
|
||||
minimap->setRotateWithCamera(false);
|
||||
}
|
||||
}
|
||||
saveSettings();
|
||||
|
|
@ -4836,7 +4840,7 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|||
float bearing = 0.0f;
|
||||
float cosB = 1.0f;
|
||||
float sinB = 0.0f;
|
||||
if (minimapRotate_) {
|
||||
if (minimap->isRotateWithCamera()) {
|
||||
glm::vec3 fwd = camera->getForward();
|
||||
bearing = std::atan2(-fwd.x, fwd.y);
|
||||
cosB = std::cos(bearing);
|
||||
|
|
@ -5115,9 +5119,9 @@ void GameScreen::loadSettings() {
|
|||
uiOpacity_ = static_cast<float>(v) / 100.0f;
|
||||
}
|
||||
} else if (key == "minimap_rotate") {
|
||||
int v = std::stoi(val);
|
||||
minimapRotate_ = (v != 0);
|
||||
pendingMinimapRotate = minimapRotate_;
|
||||
// Ignore persisted rotate state; keep north-up.
|
||||
minimapRotate_ = false;
|
||||
pendingMinimapRotate = false;
|
||||
} else if (key == "minimap_square") {
|
||||
int v = std::stoi(val);
|
||||
minimapSquare_ = (v != 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue