Fix transport update handling, add desktop/icon resources, and clean repo artifacts

This commit is contained in:
Kelsi 2026-02-11 15:24:05 -08:00
parent c2ccca9f67
commit 7fee4c91af
29 changed files with 284 additions and 41 deletions

View file

@ -888,14 +888,28 @@ void Application::setupUICallbacks() {
// Coordinates are already canonical (converted in game_handler.cpp when entity was created)
glm::vec3 canonicalSpawnPos(x, y, z);
// Check if we have a real path from TransportAnimation.dbc (indexed by entry)
// 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)) {
LOG_WARNING("No TransportAnimation.dbc path for entry ", entry,
" - transport will be stationary");
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);
} else {
uint32_t inferredPath = transportManager->inferMovingPathForSpawn(canonicalSpawnPos);
if (inferredPath != 0) {
pathId = inferredPath;
LOG_INFO("Using inferred transport path ", pathId, " for entry ", entry);
} else {
LOG_WARNING("No TransportAnimation.dbc path for entry ", entry,
" - transport will be stationary");
// Fallback: Stationary at spawn point (wait for server to send real position)
std::vector<glm::vec3> path = { canonicalSpawnPos };
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
// Fallback: Stationary at spawn point (wait for server to send real position)
std::vector<glm::vec3> path = { canonicalSpawnPos };
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
}
}
} else {
LOG_INFO("Using real transport path from TransportAnimation.dbc for entry ", entry);
}
@ -943,12 +957,28 @@ 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 create stationary fallback
// Check if we have a real path, otherwise remap/infer/fall back to stationary.
if (!transportManager->hasPathForEntry(entry)) {
std::vector<glm::vec3> path = { canonicalSpawnPos };
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
LOG_INFO("Auto-spawned transport with stationary path: entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
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 {
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 {
std::vector<glm::vec3> path = { canonicalSpawnPos };
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
LOG_INFO("Auto-spawned transport with stationary path: entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
}
}
} else {
LOG_INFO("Auto-spawned transport with real path: entry=", entry,
" displayId=", displayId, " wmoInstance=", wmoInstanceId);

View file

@ -1782,12 +1782,29 @@ void GameHandler::sendMovement(Opcode opcode) {
movementInfo.transportX = playerTransportOffset_.x;
movementInfo.transportY = playerTransportOffset_.y;
movementInfo.transportZ = playerTransportOffset_.z;
movementInfo.transportO = movementInfo.orientation; // Use same orientation as player
movementInfo.transportTime = movementInfo.time; // Use same timestamp
movementInfo.transportTime = movementInfo.time;
movementInfo.transportSeat = -1;
movementInfo.transportTime2 = movementInfo.time;
// ONTRANSPORT expects local orientation (player yaw relative to transport yaw).
float transportYaw = 0.0f;
if (transportManager_) {
if (auto* tr = transportManager_->getTransport(playerTransportGuid_); tr && tr->hasServerYaw) {
transportYaw = tr->serverYaw;
}
}
float localTransportO = movementInfo.orientation - transportYaw;
constexpr float kPi = 3.14159265359f;
constexpr float kTwoPi = 6.28318530718f;
while (localTransportO > kPi) localTransportO -= kTwoPi;
while (localTransportO < -kPi) localTransportO += kTwoPi;
movementInfo.transportO = localTransportO;
} else {
// Clear transport flag if not on transport
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
movementInfo.transportGuid = 0;
movementInfo.transportSeat = -1;
}
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
@ -1907,6 +1924,26 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
playerTransportOffset_ = glm::vec3(0.0f);
}
}
// GameObjects with UPDATEFLAG_POSITION carry a parent transport GUID and local offset.
// Use that to drive parent transport motion and compose correct child world position.
if (block.objectType == ObjectType::GAMEOBJECT &&
(block.updateFlags & 0x0100) &&
block.onTransport &&
block.transportGuid != 0) {
glm::vec3 localOffset = core::coords::serverToCanonical(
glm::vec3(block.transportX, block.transportY, block.transportZ));
// 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());
}
}
}
// Set fields
@ -2381,6 +2418,26 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
entity->setPosition(pos.x, pos.y, pos.z, block.orientation);
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
// Some GameObject movement blocks are transport-relative: the packet carries
// parent transport GUID + local child offset in UPDATEFLAG_POSITION.
if (entity->getType() == ObjectType::GAMEOBJECT &&
(block.updateFlags & 0x0100) &&
block.onTransport &&
block.transportGuid != 0) {
glm::vec3 localOffset = core::coords::serverToCanonical(
glm::vec3(block.transportX, block.transportY, block.transportZ));
if (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());
}
}
if (block.guid == playerGuid) {
movementInfo.x = pos.x;
movementInfo.y = pos.y;

View file

@ -140,6 +140,7 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
TransportPath path;
path.pathId = pathId;
path.zOnly = false; // Manually loaded paths are assumed to have XY movement
path.fromDBC = false;
// Helper: compute segment duration from distance and speed
auto segMsFromDist = [&](float dist) -> uint32_t {
@ -237,12 +238,21 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
transport.localClockMs += (uint32_t)(deltaTime * 1000.0f);
pathTimeMs = transport.localClockMs % path.durationMs;
} else {
// Server-driven but no clock yet - don't move
updateTransformMatrices(transport);
if (wmoRenderer_) {
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
// 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);
}
return;
}
return;
}
// Evaluate position from time (path is local offsets, add base position)
@ -496,6 +506,7 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
// Track server updates
transport->serverUpdateCount++;
transport->lastServerUpdate = elapsedTime_;
transport->useClientAnimation = false; // Server updates take precedence
auto pathIt = paths_.find(transport->pathId);
if (pathIt == paths_.end() || pathIt->second.durationMs == 0) {
@ -836,6 +847,7 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
path.looping = false;
path.durationMs = durationMs;
path.zOnly = isZOnly;
path.fromDBC = true;
paths_[transportEntry] = path;
pathsLoaded++;
@ -857,7 +869,95 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
}
bool TransportManager::hasPathForEntry(uint32_t entry) const {
return paths_.find(entry) != paths_.end();
auto it = paths_.find(entry);
return it != paths_.end() && it->second.fromDBC;
}
uint32_t TransportManager::inferMovingPathForSpawn(const glm::vec3& spawnWorldPos, float maxDistance) const {
float bestD2 = maxDistance * maxDistance;
uint32_t bestPathId = 0;
for (const auto& [pathId, path] : paths_) {
if (!path.fromDBC || path.durationMs == 0 || path.zOnly || path.points.empty()) {
continue;
}
// Find nearest waypoint on this path to spawn.
for (const auto& p : path.points) {
glm::vec3 diff = p.pos - spawnWorldPos;
float d2 = glm::dot(diff, diff);
if (d2 < bestD2) {
bestD2 = d2;
bestPathId = pathId;
}
}
}
if (bestPathId != 0) {
LOG_INFO("TransportManager: Inferred moving DBC path ", bestPathId,
" for spawn at (", spawnWorldPos.x, ", ", spawnWorldPos.y, ", ", spawnWorldPos.z,
"), dist=", std::sqrt(bestD2));
}
return bestPathId;
}
uint32_t TransportManager::pickFallbackMovingPath(uint32_t entry, uint32_t displayId) const {
auto isUsableMovingPath = [this](uint32_t pathId) -> bool {
auto it = paths_.find(pathId);
if (it == paths_.end()) return false;
const auto& path = it->second;
return path.fromDBC && !path.zOnly && path.durationMs > 0 && path.points.size() > 1;
};
// Known AzerothCore transport entry remaps (WotLK): server entry -> moving DBC path id.
// These entries commonly do not match TransportAnimation.dbc ids 1:1.
static const std::unordered_map<uint32_t, uint32_t> kEntryRemap = {
{176231u, 176080u}, // The Maiden's Fancy
{176310u, 176081u}, // The Bravery
{20808u, 176082u}, // The Black Princess
{164871u, 193182u}, // The Thundercaller
{176495u, 193183u}, // The Purple Princess
{175080u, 193182u}, // The Iron Eagle
{181689u, 193183u}, // Cloudkisser
{186238u, 193182u}, // The Mighty Wind
{181688u, 176083u}, // Northspear (icebreaker)
{190536u, 176084u}, // Stormwind's Pride (icebreaker)
};
auto itMapped = kEntryRemap.find(entry);
if (itMapped != kEntryRemap.end() && isUsableMovingPath(itMapped->second)) {
return itMapped->second;
}
// Fallback by display model family.
const bool looksLikeShip =
(displayId == 3015u || displayId == 2454u || displayId == 7446u || displayId == 455u || displayId == 462u);
const bool looksLikeZeppelin =
(displayId == 3031u || displayId == 7546u || displayId == 1587u || displayId == 807u || displayId == 808u);
if (looksLikeShip) {
static const uint32_t kShipCandidates[] = {176080u, 176081u, 176082u, 176083u, 176084u, 176085u, 194675u};
for (uint32_t id : kShipCandidates) {
if (isUsableMovingPath(id)) return id;
}
}
if (looksLikeZeppelin) {
static const uint32_t kZeppelinCandidates[] = {193182u, 193183u, 188360u, 190587u};
for (uint32_t id : kZeppelinCandidates) {
if (isUsableMovingPath(id)) return id;
}
}
// Last-resort: pick any moving DBC path so transport does not remain stationary.
for (const auto& [pathId, path] : paths_) {
if (path.fromDBC && !path.zOnly && path.durationMs > 0 && path.points.size() > 1) {
return pathId;
}
}
return 0;
}
} // namespace wowee::game

View file

@ -565,23 +565,8 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
// Write orientation
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
// Write pitch if swimming/flying
if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
}
// Fall time is ALWAYS present in the packet (server reads it unconditionally).
// Jump velocity/angle data is only present when FALLING flag is set.
packet.writeUInt32(info.fallTime);
if (info.hasFlag(MovementFlags::FALLING)) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
}
// Write transport data if on transport
// Write transport data if on transport.
// 3.3.5a ordering: transport block appears before pitch/fall/jump.
if (info.hasFlag(MovementFlags::ONTRANSPORT)) {
// Write packed transport GUID
uint8_t transMask = 0;
@ -607,6 +592,30 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
// Write transport time
packet.writeUInt32(info.transportTime);
// Transport seat is always present in ONTRANSPORT movement info.
packet.writeUInt8(static_cast<uint8_t>(info.transportSeat));
// Optional second transport time for interpolated movement.
if (info.flags2 & 0x0200) {
packet.writeUInt32(info.transportTime2);
}
}
// Write pitch if swimming/flying
if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
}
// Fall time is ALWAYS present in the packet (server reads it unconditionally).
// Jump velocity/angle data is only present when FALLING flag is set.
packet.writeUInt32(info.fallTime);
if (info.hasFlag(MovementFlags::FALLING)) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
}
// Detailed hex dump for debugging
@ -817,15 +826,18 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
block.x = packet.readFloat();
block.y = packet.readFloat();
block.z = packet.readFloat();
/*float transportOffsetX =*/ packet.readFloat();
/*float transportOffsetY =*/ packet.readFloat();
/*float transportOffsetZ =*/ packet.readFloat();
block.onTransport = (transportGuid != 0);
block.transportGuid = transportGuid;
block.transportX = packet.readFloat();
block.transportY = packet.readFloat();
block.transportZ = packet.readFloat();
block.orientation = packet.readFloat();
/*float corpseOrientation =*/ packet.readFloat();
block.hasMovement = true;
LOG_INFO(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec,
" pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation);
" pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation,
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
}
else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) {
// Simple stationary position (4 floats)