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 0a51ec8dda
commit c20d5441d0
29 changed files with 284 additions and 41 deletions

1
.gitignore vendored
View file

@ -67,3 +67,4 @@ cache/
# Single-player saves
saves/
wowee_[0-9][0-9][0-9][0-9]

View file

@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.15)
project(wowee VERSION 1.0.0 LANGUAGES CXX)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -264,8 +265,13 @@ set(WOWEE_HEADERS
include/ui/talent_screen.hpp
)
set(WOWEE_PLATFORM_SOURCES)
if(WIN32)
list(APPEND WOWEE_PLATFORM_SOURCES resources/wowee.rc)
endif()
# Create executable
add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS})
add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS} ${WOWEE_PLATFORM_SOURCES})
# Include directories
target_include_directories(wowee PRIVATE
@ -341,6 +347,22 @@ install(TARGETS wowee
ARCHIVE DESTINATION lib
)
# Linux desktop integration (launcher + icon)
if(UNIX AND NOT APPLE)
set(WOWEE_LINUX_ICON_PATH "${CMAKE_INSTALL_FULL_DATAROOTDIR}/icons/hicolor/256x256/apps/wowee.png")
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/resources/wowee.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/wowee.desktop
@ONLY
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/wowee.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/Wowee.png
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps
RENAME wowee.png)
endif()
# Print configuration summary
message(STATUS "")
message(STATUS "Wowee Configuration:")

View file

@ -28,6 +28,7 @@ struct TransportPath {
bool looping; // Set to false after adding explicit wrap point
uint32_t durationMs; // Total loop duration in ms (includes wrap segment if added)
bool zOnly; // True if path only has Z movement (elevator/bobbing), false if real XY travel
bool fromDBC; // True if loaded from TransportAnimation.dbc, false for runtime fallback/custom paths
};
struct ActiveTransport {
@ -85,6 +86,15 @@ public:
// Check if a path exists for a given GameObject entry
bool hasPathForEntry(uint32_t entry) 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).
// Returns 0 when no suitable path match is found.
uint32_t inferMovingPathForSpawn(const glm::vec3& spawnWorldPos, float maxDistance = 1200.0f) const;
// Choose a deterministic fallback moving DBC path for known server transport entries/displayIds.
// Returns 0 when no suitable moving path is available.
uint32_t pickFallbackMovingPath(uint32_t entry, uint32_t displayId) const;
// Update server-controlled transport position/rotation directly (bypasses path movement)
void updateServerTransport(uint64_t guid, const glm::vec3& position, float orientation);

View file

@ -418,6 +418,8 @@ struct MovementInfo {
float transportZ = 0.0f;
float transportO = 0.0f; // Local orientation on transport
uint32_t transportTime = 0; // Transport movement timestamp
int8_t transportSeat = -1; // Transport seat (-1 when unknown/not seated)
uint32_t transportTime2 = 0; // Secondary transport time (when interpolated movement flag is set)
bool hasFlag(MovementFlags flag) const {
return (flags & static_cast<uint32_t>(flag)) != 0;

View file

@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Wowee
Comment=Custom World of Warcraft WotLK compatible game client
Exec=wowee
Icon=@WOWEE_LINUX_ICON_PATH@
Terminal=false
Categories=Game;

1
resources/wowee.rc Normal file
View file

@ -0,0 +1 @@
IDI_APP_ICON ICON "assets\\wowee.ico"

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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.