mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Transport hell
This commit is contained in:
parent
2e923311d0
commit
f3f3b62880
12 changed files with 912 additions and 126 deletions
|
|
@ -87,7 +87,7 @@ private:
|
|||
void despawnOnlineCreature(uint64_t guid);
|
||||
void buildCreatureDisplayLookups();
|
||||
std::string getModelPathForDisplayId(uint32_t displayId) const;
|
||||
void spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void despawnOnlineGameObject(uint64_t guid);
|
||||
void buildGameObjectDisplayLookups();
|
||||
std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const;
|
||||
|
|
@ -197,6 +197,7 @@ private:
|
|||
|
||||
struct PendingGameObjectSpawn {
|
||||
uint64_t guid;
|
||||
uint32_t entry;
|
||||
uint32_t displayId;
|
||||
float x, y, z, orientation;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -462,8 +462,8 @@ public:
|
|||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject spawn callback (online mode - triggered when gameobject enters view)
|
||||
// Parameters: guid, displayId, x, y, z (canonical), orientation
|
||||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation
|
||||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject despawn callback (online mode - triggered when gameobject leaves view)
|
||||
|
|
@ -483,10 +483,25 @@ public:
|
|||
using TransportMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, float orientation)>;
|
||||
void setTransportMoveCallback(TransportMoveCallback cb) { transportMoveCallback_ = std::move(cb); }
|
||||
|
||||
// Transport spawn callback (online mode - triggered when transport GameObject is first detected)
|
||||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation
|
||||
using TransportSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
void setTransportSpawnCallback(TransportSpawnCallback cb) { transportSpawnCallback_ = std::move(cb); }
|
||||
|
||||
// Notify that a transport has been spawned (called after WMO instance creation)
|
||||
void notifyTransportSpawned(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
if (transportSpawnCallback_) {
|
||||
transportSpawnCallback_(guid, entry, displayId, x, y, z, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
// Transport state for player-on-transport
|
||||
bool isOnTransport() const { return playerTransportGuid_ != 0; }
|
||||
uint64_t getPlayerTransportGuid() const { return playerTransportGuid_; }
|
||||
glm::vec3 getPlayerTransportOffset() const { return playerTransportOffset_; }
|
||||
|
||||
// Check if a GUID is a known transport
|
||||
bool isTransportGuid(uint64_t guid) const { return transportGuids_.count(guid) > 0; }
|
||||
glm::vec3 getComposedWorldPosition(); // Compose transport transform * local offset
|
||||
TransportManager* getTransportManager() { return transportManager_.get(); }
|
||||
void setPlayerOnTransport(uint64_t transportGuid, const glm::vec3& localOffset) {
|
||||
|
|
@ -792,6 +807,7 @@ private:
|
|||
|
||||
// ---- Creature movement handler ----
|
||||
void handleMonsterMove(network::Packet& packet);
|
||||
void handleMonsterMoveTransport(network::Packet& packet);
|
||||
|
||||
// ---- Phase 5 handlers ----
|
||||
void handleLootResponse(network::Packet& packet);
|
||||
|
|
@ -979,6 +995,7 @@ private:
|
|||
CreatureDespawnCallback creatureDespawnCallback_;
|
||||
CreatureMoveCallback creatureMoveCallback_;
|
||||
TransportMoveCallback transportMoveCallback_;
|
||||
TransportSpawnCallback transportSpawnCallback_;
|
||||
GameObjectSpawnCallback gameObjectSpawnCallback_;
|
||||
GameObjectDespawnCallback gameObjectDespawnCallback_;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ enum class Opcode : uint16_t {
|
|||
// ---- Entity/Object updates ----
|
||||
SMSG_UPDATE_OBJECT = 0x0A9,
|
||||
SMSG_COMPRESSED_UPDATE_OBJECT = 0x1F6,
|
||||
SMSG_MONSTER_MOVE_TRANSPORT = 0x2AE,
|
||||
SMSG_DESTROY_OBJECT = 0x0AA,
|
||||
|
||||
// ---- Chat ----
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
|
|
@ -10,22 +11,29 @@ namespace wowee::rendering {
|
|||
class WMORenderer;
|
||||
}
|
||||
|
||||
namespace wowee::pipeline {
|
||||
class AssetManager;
|
||||
}
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
struct TimedPoint {
|
||||
uint32_t tMs; // Time in milliseconds from DBC
|
||||
glm::vec3 pos; // Position at this time
|
||||
};
|
||||
|
||||
struct TransportPath {
|
||||
uint32_t pathId;
|
||||
std::vector<glm::vec3> waypoints; // Position keyframes
|
||||
std::vector<glm::quat> rotations; // Optional rotation keyframes
|
||||
bool looping;
|
||||
float speed; // units/sec (default 18.0f like taxi)
|
||||
std::vector<TimedPoint> points; // Time-indexed waypoints (includes duplicate first point at end for wrap)
|
||||
bool looping; // Set to false after adding explicit wrap point
|
||||
uint32_t durationMs; // Total loop duration in ms (includes wrap segment if added)
|
||||
};
|
||||
|
||||
struct ActiveTransport {
|
||||
uint64_t guid; // Entity GUID
|
||||
uint32_t wmoInstanceId; // WMO renderer instance ID
|
||||
uint32_t pathId; // Current path
|
||||
size_t currentSegment; // Current waypoint index
|
||||
float segmentProgress; // Distance along segment
|
||||
glm::vec3 basePosition; // Spawn position (base offset for path)
|
||||
glm::vec3 position; // Current world position
|
||||
glm::quat rotation; // Current world rotation
|
||||
glm::mat4 transform; // Cached world transform
|
||||
|
|
@ -39,6 +47,17 @@ struct ActiveTransport {
|
|||
glm::vec3 deckMin;
|
||||
glm::vec3 deckMax;
|
||||
bool hasDeckBounds;
|
||||
|
||||
// Time-based animation (deterministic, no drift)
|
||||
uint32_t localClockMs; // Local path time in milliseconds
|
||||
bool hasServerClock; // Whether we've synced with server time
|
||||
int32_t serverClockOffsetMs; // Offset: serverClock - localNow
|
||||
bool useClientAnimation; // Use client-side path animation
|
||||
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
|
||||
};
|
||||
|
||||
class TransportManager {
|
||||
|
|
@ -49,7 +68,7 @@ public:
|
|||
void setWMORenderer(rendering::WMORenderer* renderer) { wmoRenderer_ = renderer; }
|
||||
|
||||
void update(float deltaTime);
|
||||
void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId);
|
||||
void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos);
|
||||
void unregisterTransport(uint64_t guid);
|
||||
|
||||
ActiveTransport* getTransport(uint64_t guid);
|
||||
|
|
@ -59,15 +78,30 @@ public:
|
|||
void loadPathFromNodes(uint32_t pathId, const std::vector<glm::vec3>& waypoints, bool looping = true, float speed = 18.0f);
|
||||
void setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max);
|
||||
|
||||
// Load transport paths from TransportAnimation.dbc
|
||||
bool loadTransportAnimationDBC(pipeline::AssetManager* assetMgr);
|
||||
|
||||
// Check if a path exists for a given GameObject entry
|
||||
bool hasPathForEntry(uint32_t entry) const;
|
||||
|
||||
// Update server-controlled transport position/rotation directly (bypasses path movement)
|
||||
void updateServerTransport(uint64_t guid, const glm::vec3& position, float orientation);
|
||||
|
||||
// Enable/disable client-side animation for transports without server updates
|
||||
void setClientSideAnimation(bool enabled) { clientSideAnimation_ = enabled; }
|
||||
bool isClientSideAnimation() const { return clientSideAnimation_; }
|
||||
|
||||
private:
|
||||
void updateTransportMovement(ActiveTransport& transport, float deltaTime);
|
||||
glm::vec3 interpolatePath(const TransportPath& path, size_t segmentIdx, float t);
|
||||
glm::quat calculateOrientation(const TransportPath& path, size_t segmentIdx, float t);
|
||||
glm::vec3 evalTimedCatmullRom(const TransportPath& path, uint32_t pathTimeMs);
|
||||
glm::quat orientationFromTangent(const TransportPath& path, uint32_t pathTimeMs);
|
||||
void updateTransformMatrices(ActiveTransport& transport);
|
||||
|
||||
std::unordered_map<uint64_t, ActiveTransport> transports_;
|
||||
std::unordered_map<uint32_t, TransportPath> paths_;
|
||||
std::unordered_map<uint32_t, TransportPath> paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc)
|
||||
rendering::WMORenderer* wmoRenderer_ = nullptr;
|
||||
bool clientSideAnimation_ = true; // Enable client animation - smooth movement, synced with server updates
|
||||
float elapsedTime_ = 0.0f; // Total elapsed time (seconds)
|
||||
};
|
||||
|
||||
} // namespace wowee::game
|
||||
|
|
|
|||
|
|
@ -252,6 +252,13 @@ public:
|
|||
*/
|
||||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||
|
||||
/**
|
||||
* Update the full transform of an existing instance (e.g., for WMO doodads following parent WMO)
|
||||
* @param instanceId Instance ID returned by createInstance()
|
||||
* @param transform New world transform matrix
|
||||
*/
|
||||
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
||||
|
||||
/**
|
||||
* Remove a specific instance by ID
|
||||
* @param instanceId Instance ID returned by createInstance()
|
||||
|
|
|
|||
|
|
@ -406,6 +406,13 @@ private:
|
|||
glm::vec3 worldBoundsMax;
|
||||
std::vector<std::pair<glm::vec3, glm::vec3>> worldGroupBounds;
|
||||
|
||||
// Doodad tracking: M2 instances that are children of this WMO
|
||||
struct DoodadInfo {
|
||||
uint32_t m2InstanceId; // ID of the M2 instance
|
||||
glm::mat4 localTransform; // Local transform relative to WMO origin
|
||||
};
|
||||
std::vector<DoodadInfo> doodads;
|
||||
|
||||
void updateModelMatrix();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,11 @@ bool Application::initialize() {
|
|||
LOG_INFO("Asset manager initialized successfully");
|
||||
// Eagerly load creature display DBC lookups so first spawn doesn't stall
|
||||
buildCreatureDisplayLookups();
|
||||
|
||||
// Load transport paths from TransportAnimation.dbc
|
||||
if (gameHandler && gameHandler->getTransportManager()) {
|
||||
gameHandler->getTransportManager()->loadTransportAnimationDBC(assetManager.get());
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable");
|
||||
LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory");
|
||||
|
|
@ -486,6 +491,14 @@ void Application::update(float deltaTime) {
|
|||
if (renderer && gameHandler) {
|
||||
bool onTransport = gameHandler->isOnTransport();
|
||||
|
||||
// Debug: Log transport state changes
|
||||
static bool wasOnTransport = false;
|
||||
if (onTransport != wasOnTransport) {
|
||||
LOG_INFO("Transport state changed: onTransport=", onTransport,
|
||||
" guid=0x", std::hex, gameHandler->getPlayerTransportGuid(), std::dec);
|
||||
wasOnTransport = onTransport;
|
||||
}
|
||||
|
||||
if (onTaxi) {
|
||||
auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid());
|
||||
if (playerEntity) {
|
||||
|
|
@ -757,8 +770,8 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.)
|
||||
gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
pendingGameObjectSpawns_.push_back({guid, displayId, x, y, z, orientation});
|
||||
gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation});
|
||||
});
|
||||
|
||||
// GameObject despawn callback (online mode) - remove static models
|
||||
|
|
@ -848,34 +861,134 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
});
|
||||
|
||||
// Transport move callback (online mode) - update transport gameobject positions
|
||||
gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float /*orientation*/) {
|
||||
// Transport spawn callback (online mode) - register transports with TransportManager
|
||||
gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
auto* transportManager = gameHandler->getTransportManager();
|
||||
if (!transportManager || !renderer) return;
|
||||
|
||||
// Get the WMO instance ID from the GameObject spawn
|
||||
auto it = gameObjectInstances_.find(guid);
|
||||
if (it == gameObjectInstances_.end()) return;
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
if (renderer) {
|
||||
if (it->second.isWmo) {
|
||||
if (auto* wmoRenderer = renderer->getWMORenderer()) {
|
||||
wmoRenderer->setInstancePosition(it->second.instanceId, renderPos);
|
||||
if (it == gameObjectInstances_.end()) {
|
||||
LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t wmoInstanceId = it->second.instanceId;
|
||||
LOG_INFO("Registering server transport: GUID=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId,
|
||||
" pos=(", x, ", ", y, ", ", z, ")");
|
||||
|
||||
// TransportAnimation.dbc is indexed by GameObject entry
|
||||
uint32_t pathId = entry;
|
||||
|
||||
bool clientAnim = transportManager->isClientSideAnimation();
|
||||
LOG_INFO("Transport spawn callback: clientAnimation=", clientAnim,
|
||||
" guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId);
|
||||
|
||||
// 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)
|
||||
if (!transportManager->hasPathForEntry(entry)) {
|
||||
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);
|
||||
} else {
|
||||
LOG_INFO("Using real transport path from TransportAnimation.dbc for entry ", entry);
|
||||
}
|
||||
|
||||
// Register the transport with spawn position (prevents rendering at origin until server update)
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos);
|
||||
|
||||
if (clientAnim) {
|
||||
LOG_INFO("Transport registered - client-side animation enabled");
|
||||
} else {
|
||||
// Only call updateServerTransport if client animation is disabled
|
||||
// This sets the exact spawn position for server-controlled transports
|
||||
// Coordinates are already canonical (converted in game_handler.cpp)
|
||||
glm::vec3 canonicalPos(x, y, z);
|
||||
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
||||
LOG_INFO("Transport registered - server-controlled movement");
|
||||
}
|
||||
});
|
||||
|
||||
// Transport move callback (online mode) - update transport gameobject positions
|
||||
gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float orientation) {
|
||||
LOG_INFO("Transport move callback: GUID=0x", std::hex, guid, std::dec,
|
||||
" pos=(", x, ", ", y, ", ", z, ") orientation=", orientation);
|
||||
|
||||
auto* transportManager = gameHandler->getTransportManager();
|
||||
if (!transportManager) {
|
||||
LOG_WARNING("Transport move callback: TransportManager is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if transport exists - if not, treat this as a late spawn (reconnection/server restart)
|
||||
if (!transportManager->getTransport(guid)) {
|
||||
LOG_INFO("Received position update for unregistered transport 0x", std::hex, guid, std::dec,
|
||||
" - auto-spawning from position update");
|
||||
|
||||
// Get transport info from entity manager
|
||||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||
if (entity && entity->getType() == game::ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
uint32_t entry = go->getEntry();
|
||||
uint32_t displayId = go->getDisplayId();
|
||||
|
||||
// Find the WMO instance for this transport (should exist from earlier GameObject spawn)
|
||||
auto it = gameObjectInstances_.find(guid);
|
||||
if (it != gameObjectInstances_.end()) {
|
||||
uint32_t wmoInstanceId = it->second.instanceId;
|
||||
|
||||
// TransportAnimation.dbc is indexed by GameObject entry
|
||||
uint32_t pathId = entry;
|
||||
|
||||
// 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
|
||||
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);
|
||||
} else {
|
||||
LOG_INFO("Auto-spawned transport with real path: entry=", entry,
|
||||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
}
|
||||
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos);
|
||||
} else {
|
||||
LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
|
||||
" - WMO instance not found");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (auto* m2Renderer = renderer->getM2Renderer()) {
|
||||
m2Renderer->setInstancePosition(it->second.instanceId, renderPos);
|
||||
}
|
||||
LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
|
||||
" - entity not found in EntityManager");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Move player with transport if riding it
|
||||
if (gameHandler && gameHandler->isOnTransport() && gameHandler->getPlayerTransportGuid() == guid) {
|
||||
auto* cc = renderer->getCameraController();
|
||||
if (cc) {
|
||||
glm::vec3* ft = cc->getFollowTargetMutable();
|
||||
if (ft) {
|
||||
// Transport offset is in server/canonical coords — convert to render
|
||||
glm::vec3 offset = gameHandler->getPlayerTransportOffset();
|
||||
glm::vec3 canonicalPlayerPos = glm::vec3(x + offset.x, y + offset.y, z + offset.z);
|
||||
glm::vec3 playerRenderPos = core::coords::canonicalToRender(canonicalPlayerPos);
|
||||
*ft = playerRenderPos;
|
||||
}
|
||||
// Update TransportManager's internal state (position, rotation, transform matrices)
|
||||
// This also updates the WMO renderer automatically
|
||||
// Coordinates are already canonical (converted in game_handler.cpp when entity was created)
|
||||
glm::vec3 canonicalPos(x, y, z);
|
||||
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
||||
|
||||
// Move player with transport if riding it
|
||||
if (gameHandler && gameHandler->isOnTransport() && gameHandler->getPlayerTransportGuid() == guid && renderer) {
|
||||
auto* cc = renderer->getCameraController();
|
||||
if (cc) {
|
||||
glm::vec3* ft = cc->getFollowTargetMutable();
|
||||
if (ft) {
|
||||
// Get player world position from TransportManager (handles transform properly)
|
||||
glm::vec3 offset = gameHandler->getPlayerTransportOffset();
|
||||
glm::vec3 worldPos = transportManager->getPlayerWorldPosition(guid, offset);
|
||||
*ft = worldPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1673,6 +1786,12 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
renderer->getTerrainManager()->setMapName(mapName);
|
||||
}
|
||||
|
||||
// Connect TransportManager to WMORenderer (for server transports)
|
||||
if (gameHandler && gameHandler->getTransportManager() && renderer->getWMORenderer()) {
|
||||
gameHandler->getTransportManager()->setWMORenderer(renderer->getWMORenderer());
|
||||
LOG_INFO("TransportManager connected to WMORenderer for online mode");
|
||||
}
|
||||
|
||||
showProgress("Loading character model...", 0.05f);
|
||||
|
||||
// Build faction hostility map for this character's race
|
||||
|
|
@ -2599,7 +2718,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
}
|
||||
|
||||
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
if (!renderer || !assetManager) return;
|
||||
|
||||
if (!gameObjectLookupsBuilt_) {
|
||||
|
|
@ -2625,7 +2744,36 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
|
|||
return;
|
||||
}
|
||||
|
||||
std::string modelPath = getGameObjectModelPathForDisplayId(displayId);
|
||||
std::string modelPath;
|
||||
|
||||
// Override model path for transports with wrong displayIds (preloaded transports)
|
||||
// Check if this GUID is a known transport
|
||||
bool isTransport = gameHandler && gameHandler->isTransportGuid(guid);
|
||||
if (isTransport) {
|
||||
// Map common transport displayIds to correct WMO paths
|
||||
// DisplayIds 455, 462 = Elevators/Ships → try standard ship
|
||||
// DisplayIds 807, 808 = Zeppelins
|
||||
// DisplayIds 2454, 1587 = Special ships/icebreakers
|
||||
if (displayId == 455 || displayId == 462 || displayId == 20808 || displayId == 176231 || displayId == 176310) {
|
||||
modelPath = "World\\wmo\\transports\\transport_ship\\transportship.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → transportship.wmo");
|
||||
} else if (displayId == 807 || displayId == 808 || displayId == 175080 || displayId == 176495 || displayId == 164871) {
|
||||
modelPath = "World\\wmo\\transports\\transport_zeppelin\\transport_zeppelin.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → transport_zeppelin.wmo");
|
||||
} else if (displayId == 1587) {
|
||||
modelPath = "World\\wmo\\transports\\transport_horde_zeppelin\\Transport_Horde_Zeppelin.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → Transport_Horde_Zeppelin.wmo");
|
||||
} else if (displayId == 2454 || displayId == 181688 || displayId == 190536) {
|
||||
modelPath = "World\\wmo\\transports\\icebreaker\\Transport_Icebreaker_ship.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → Transport_Icebreaker_ship.wmo");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to normal displayId lookup if not a transport or no override matched
|
||||
if (modelPath.empty()) {
|
||||
modelPath = getGameObjectModelPathForDisplayId(displayId);
|
||||
}
|
||||
|
||||
if (modelPath.empty()) {
|
||||
LOG_WARNING("No model path for gameobject displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")");
|
||||
return;
|
||||
|
|
@ -2721,6 +2869,18 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
|
|||
gameObjectInstances_[guid] = {modelId, instanceId, true};
|
||||
LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
|
||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
|
||||
// Check if this is a transport and notify via special method
|
||||
if (gameHandler) {
|
||||
std::string lowerModelPath = modelPath;
|
||||
std::transform(lowerModelPath.begin(), lowerModelPath.end(), lowerModelPath.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowerModelPath.find("transport") != std::string::npos) {
|
||||
// This is a transport GameObject - notify the game handler
|
||||
gameHandler->notifyTransportSpawned(guid, entry, displayId, x, y, z, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2798,7 +2958,7 @@ void Application::processGameObjectSpawnQueue() {
|
|||
int spawned = 0;
|
||||
while (!pendingGameObjectSpawns_.empty() && spawned < MAX_SPAWNS_PER_FRAME) {
|
||||
auto& s = pendingGameObjectSpawns_.front();
|
||||
spawnOnlineGameObject(s.guid, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||
pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin());
|
||||
spawned++;
|
||||
}
|
||||
|
|
@ -3214,7 +3374,7 @@ void Application::setupTestTransport() {
|
|||
|
||||
// Register transport with transport manager
|
||||
uint64_t transportGuid = 0x1000000000000001ULL; // Fake GUID for test
|
||||
transportManager->registerTransport(transportGuid, wmoInstanceId, pathId);
|
||||
transportManager->registerTransport(transportGuid, wmoInstanceId, pathId, startCanonical);
|
||||
|
||||
// Optional: Set deck bounds (rough estimate for a ship deck)
|
||||
transportManager->setDeckBounds(transportGuid,
|
||||
|
|
|
|||
|
|
@ -601,6 +601,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleMonsterMove(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_MONSTER_MOVE_TRANSPORT:
|
||||
handleMonsterMoveTransport(packet);
|
||||
break;
|
||||
|
||||
// ---- Speed Changes ----
|
||||
case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE:
|
||||
handleForceRunSpeedChange(packet);
|
||||
|
|
@ -2007,9 +2011,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec,
|
||||
" displayId=", go->getDisplayId(),
|
||||
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
|
||||
// Note: TransportSpawnCallback will be invoked from Application after WMO instance is created
|
||||
}
|
||||
if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) {
|
||||
gameObjectSpawnCallback_(block.guid, go->getDisplayId(),
|
||||
gameObjectSpawnCallback_(block.guid, go->getEntry(), go->getDisplayId(),
|
||||
go->getX(), go->getY(), go->getZ(), go->getOrientation());
|
||||
}
|
||||
// Fire transport move callback for transports (position update on re-creation)
|
||||
|
|
@ -2337,6 +2342,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
|
||||
case UpdateType::MOVEMENT: {
|
||||
// Diagnostic: Log if we receive MOVEMENT blocks for transports
|
||||
if (transportGuids_.count(block.guid)) {
|
||||
LOG_INFO("MOVEMENT update for transport 0x", std::hex, block.guid, std::dec,
|
||||
" pos=(", block.x, ", ", block.y, ", ", block.z, ")");
|
||||
}
|
||||
|
||||
// Update entity position (server → canonical)
|
||||
auto entity = entityManager.getEntity(block.guid);
|
||||
if (entity) {
|
||||
|
|
@ -4247,6 +4258,49 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleMonsterMoveTransport(network::Packet& packet) {
|
||||
// Parse transport-relative creature movement (NPCs on boats/zeppelins)
|
||||
// Packet structure: mover GUID + transport GUID + spline data (local coords)
|
||||
|
||||
uint64_t moverGuid = packet.readUInt64();
|
||||
uint8_t unk = packet.readUInt8(); // Unknown byte (usually 0)
|
||||
uint64_t transportGuid = packet.readUInt64();
|
||||
|
||||
// Transport-local coordinates
|
||||
float localX = packet.readFloat();
|
||||
float localY = packet.readFloat();
|
||||
float localZ = packet.readFloat();
|
||||
|
||||
LOG_INFO("SMSG_MONSTER_MOVE_TRANSPORT: mover=0x", std::hex, moverGuid,
|
||||
" transport=0x", transportGuid, std::dec,
|
||||
" localPos=(", localX, ", ", localY, ", ", localZ, ")");
|
||||
|
||||
// Compose world position: worldPos = transportTransform * localPos
|
||||
auto entity = entityManager.getEntity(moverGuid);
|
||||
if (!entity) {
|
||||
LOG_WARNING(" NPC 0x", std::hex, moverGuid, std::dec, " not found in entity manager");
|
||||
return;
|
||||
}
|
||||
|
||||
if (transportManager_) {
|
||||
// Use TransportManager to compose world position from local offset
|
||||
glm::vec3 localPos(localX, localY, localZ);
|
||||
glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localPos);
|
||||
|
||||
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
|
||||
|
||||
LOG_INFO(" Composed NPC world position: (", worldPos.x, ", ", worldPos.y, ", ", worldPos.z, ")");
|
||||
} else {
|
||||
LOG_WARNING(" TransportManager not available for NPC position composition");
|
||||
}
|
||||
|
||||
// TODO: Parse full spline data for smooth NPC movement on transport
|
||||
// Then update entity position and call creatureMoveCallback_
|
||||
|
||||
// Suppress unused variable warning for now
|
||||
(void)unk;
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||
AttackerStateUpdateData data;
|
||||
if (!AttackerStateUpdateParser::parse(packet, data)) return;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
#include "game/transport_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
|
|
@ -11,12 +17,16 @@ TransportManager::TransportManager() = default;
|
|||
TransportManager::~TransportManager() = default;
|
||||
|
||||
void TransportManager::update(float deltaTime) {
|
||||
elapsedTime_ += deltaTime;
|
||||
|
||||
for (auto& [guid, transport] : transports_) {
|
||||
// Once we have server clock offset, we can predict server time indefinitely
|
||||
// No need for watchdog - keep using the offset even if server updates stop
|
||||
updateTransportMovement(transport, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId) {
|
||||
void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos) {
|
||||
auto pathIt = paths_.find(pathId);
|
||||
if (pathIt == paths_.end()) {
|
||||
std::cerr << "TransportManager: Path " << pathId << " not found for transport " << guid << std::endl;
|
||||
|
|
@ -24,7 +34,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
}
|
||||
|
||||
const auto& path = pathIt->second;
|
||||
if (path.waypoints.empty()) {
|
||||
if (path.points.empty()) {
|
||||
std::cerr << "TransportManager: Path " << pathId << " has no waypoints" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
|
@ -33,20 +43,49 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.guid = guid;
|
||||
transport.wmoInstanceId = wmoInstanceId;
|
||||
transport.pathId = pathId;
|
||||
transport.currentSegment = 0;
|
||||
transport.segmentProgress = 0.0f;
|
||||
transport.position = path.waypoints[0];
|
||||
|
||||
// CRITICAL: Set basePosition from spawn position and t=0 offset
|
||||
// For stationary paths (1 waypoint), just use spawn position directly
|
||||
if (path.durationMs == 0 || path.points.size() <= 1) {
|
||||
// Stationary transport - no path animation
|
||||
transport.basePosition = spawnWorldPos;
|
||||
transport.position = spawnWorldPos;
|
||||
} else {
|
||||
// Moving transport - infer base from first path offset
|
||||
glm::vec3 offset0 = evalTimedCatmullRom(path, 0);
|
||||
transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn
|
||||
transport.position = spawnWorldPos; // Start at spawn position (base + offset0)
|
||||
}
|
||||
|
||||
transport.rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion
|
||||
transport.playerOnBoard = false;
|
||||
transport.playerLocalOffset = glm::vec3(0.0f);
|
||||
transport.hasDeckBounds = false;
|
||||
transport.localClockMs = 0;
|
||||
transport.hasServerClock = false;
|
||||
transport.serverClockOffsetMs = 0;
|
||||
transport.useClientAnimation = clientSideAnimation_; // Enable client animation by default
|
||||
transport.serverYaw = 0.0f;
|
||||
transport.hasServerYaw = false;
|
||||
transport.lastServerUpdate = 0.0f;
|
||||
transport.serverUpdateCount = 0;
|
||||
|
||||
updateTransformMatrices(transport);
|
||||
|
||||
// CRITICAL: Update WMO renderer with initial transform
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
|
||||
transports_[guid] = transport;
|
||||
|
||||
std::cout << "TransportManager: Registered transport " << guid
|
||||
<< " at path " << pathId << " with " << path.waypoints.size() << " waypoints" << std::endl;
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
||||
LOG_INFO("TransportManager: Registered transport 0x", std::hex, guid, std::dec,
|
||||
" at path ", pathId, " with ", path.points.size(), " waypoints",
|
||||
" wmoInstanceId=", wmoInstanceId,
|
||||
" spawnPos=(", spawnWorldPos.x, ", ", spawnWorldPos.y, ", ", spawnWorldPos.z, ")",
|
||||
" basePos=(", transport.basePosition.x, ", ", transport.basePosition.y, ", ", transport.basePosition.z, ")",
|
||||
" initialRenderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")");
|
||||
}
|
||||
|
||||
void TransportManager::unregisterTransport(uint64_t guid) {
|
||||
|
|
@ -89,15 +128,33 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
|
|||
|
||||
TransportPath path;
|
||||
path.pathId = pathId;
|
||||
path.waypoints = waypoints;
|
||||
path.looping = looping;
|
||||
path.speed = speed;
|
||||
|
||||
// Convert waypoints to TimedPoint (for fallback paths, use arbitrary timing)
|
||||
// Single point = stationary (durationMs = 0)
|
||||
// Multiple points = assume constant speed
|
||||
path.points.reserve(waypoints.size());
|
||||
if (waypoints.size() == 1) {
|
||||
path.points.push_back({0, waypoints[0]});
|
||||
path.durationMs = 0; // Stationary
|
||||
} else {
|
||||
// Calculate cumulative time based on distance and speed
|
||||
uint32_t cumulativeMs = 0;
|
||||
path.points.push_back({0, waypoints[0]});
|
||||
for (size_t i = 1; i < waypoints.size(); i++) {
|
||||
float dist = glm::distance(waypoints[i-1], waypoints[i]);
|
||||
uint32_t segmentMs = speed > 0.0f ? (uint32_t)((dist / speed) * 1000.0f) : 1000;
|
||||
cumulativeMs += segmentMs;
|
||||
path.points.push_back({cumulativeMs, waypoints[i]});
|
||||
}
|
||||
path.durationMs = cumulativeMs;
|
||||
}
|
||||
|
||||
paths_[pathId] = path;
|
||||
|
||||
std::cout << "TransportManager: Loaded path " << pathId
|
||||
<< " with " << waypoints.size() << " waypoints, "
|
||||
<< "looping=" << looping << ", speed=" << speed << std::endl;
|
||||
LOG_INFO("TransportManager: Loaded path ", pathId,
|
||||
" with ", waypoints.size(), " waypoints, "
|
||||
"looping=", looping, ", speed=", speed);
|
||||
}
|
||||
|
||||
void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) {
|
||||
|
|
@ -119,60 +176,54 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
}
|
||||
|
||||
const auto& path = pathIt->second;
|
||||
if (path.waypoints.size() < 2) {
|
||||
return; // Need at least 2 waypoints to move
|
||||
}
|
||||
|
||||
// Calculate segment length
|
||||
glm::vec3 p0 = path.waypoints[transport.currentSegment];
|
||||
size_t nextIdx = (transport.currentSegment + 1) % path.waypoints.size();
|
||||
glm::vec3 p1 = path.waypoints[nextIdx];
|
||||
float segmentLength = glm::distance(p0, p1);
|
||||
|
||||
if (segmentLength < 0.001f) {
|
||||
// Zero-length segment, skip to next
|
||||
transport.currentSegment = nextIdx;
|
||||
transport.segmentProgress = 0.0f;
|
||||
if (path.points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update progress
|
||||
float distanceThisFrame = path.speed * deltaTime;
|
||||
transport.segmentProgress += distanceThisFrame;
|
||||
|
||||
// Check if we've completed this segment
|
||||
while (transport.segmentProgress >= segmentLength) {
|
||||
transport.segmentProgress -= segmentLength;
|
||||
transport.currentSegment = nextIdx;
|
||||
|
||||
// Check for path completion
|
||||
if (!path.looping && transport.currentSegment >= path.waypoints.size() - 1) {
|
||||
// Reached end of non-looping path
|
||||
transport.currentSegment = path.waypoints.size() - 1;
|
||||
transport.segmentProgress = 0.0f;
|
||||
transport.position = path.waypoints[transport.currentSegment];
|
||||
updateTransformMatrices(transport);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update for next segment
|
||||
p0 = path.waypoints[transport.currentSegment];
|
||||
nextIdx = (transport.currentSegment + 1) % path.waypoints.size();
|
||||
p1 = path.waypoints[nextIdx];
|
||||
segmentLength = glm::distance(p0, p1);
|
||||
|
||||
if (segmentLength < 0.001f) {
|
||||
transport.segmentProgress = 0.0f;
|
||||
continue;
|
||||
// Stationary transport (durationMs = 0)
|
||||
if (path.durationMs == 0) {
|
||||
// Just update transform (position already set)
|
||||
updateTransformMatrices(transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Interpolate position
|
||||
float t = transport.segmentProgress / segmentLength;
|
||||
transport.position = interpolatePath(path, transport.currentSegment, t);
|
||||
// Evaluate path time
|
||||
uint32_t nowMs = (uint32_t)(elapsedTime_ * 1000.0f);
|
||||
uint32_t pathTimeMs = 0;
|
||||
|
||||
// Calculate orientation from path tangent
|
||||
transport.rotation = calculateOrientation(path, transport.currentSegment, t);
|
||||
if (transport.hasServerClock) {
|
||||
// Predict server time using clock offset (works for both client and server-driven modes)
|
||||
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;
|
||||
pathTimeMs = (uint32_t)wrapped;
|
||||
} else if (transport.useClientAnimation) {
|
||||
// Pure local clock (no server sync yet, client-driven)
|
||||
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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate position from time (path is local offsets, add base position)
|
||||
glm::vec3 pathOffset = evalTimedCatmullRom(path, pathTimeMs);
|
||||
transport.position = transport.basePosition + pathOffset;
|
||||
|
||||
// Use server yaw if available (authoritative), otherwise compute from tangent
|
||||
if (transport.hasServerYaw) {
|
||||
transport.rotation = glm::angleAxis(transport.serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
} else {
|
||||
transport.rotation = orientationFromTangent(path, pathTimeMs);
|
||||
}
|
||||
|
||||
// Update transform matrices
|
||||
updateTransformMatrices(transport);
|
||||
|
|
@ -181,11 +232,51 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
|
||||
// Debug logging every 120 frames (~2 seconds at 60fps)
|
||||
static int debugFrameCount = 0;
|
||||
if (debugFrameCount++ % 120 == 0) {
|
||||
// Log canonical position AND render position to check coordinate conversion
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
||||
LOG_INFO("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" pathTime=", pathTimeMs, "ms / ", path.durationMs, "ms",
|
||||
" canonicalPos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")",
|
||||
" renderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")",
|
||||
" basePos=(", transport.basePosition.x, ", ", transport.basePosition.y, ", ", transport.basePosition.z, ")",
|
||||
" pathOffset=(", pathOffset.x, ", ", pathOffset.y, ", ", pathOffset.z, ")",
|
||||
" mode=", (transport.useClientAnimation ? "client" : "server"),
|
||||
" hasServerClock=", transport.hasServerClock,
|
||||
" offset=", transport.serverClockOffsetMs, "ms");
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 TransportManager::interpolatePath(const TransportPath& path, size_t segmentIdx, float t) {
|
||||
// Catmull-Rom spline interpolation (same as taxi flights)
|
||||
size_t numPoints = path.waypoints.size();
|
||||
glm::vec3 TransportManager::evalTimedCatmullRom(const TransportPath& path, uint32_t pathTimeMs) {
|
||||
if (path.points.empty()) {
|
||||
return glm::vec3(0.0f);
|
||||
}
|
||||
if (path.points.size() == 1) {
|
||||
return path.points[0].pos;
|
||||
}
|
||||
|
||||
// Find the segment containing pathTimeMs
|
||||
size_t segmentIdx = 0;
|
||||
bool found = false;
|
||||
|
||||
for (size_t i = 0; i + 1 < path.points.size(); i++) {
|
||||
if (pathTimeMs >= path.points[i].tMs && pathTimeMs < path.points[i + 1].tMs) {
|
||||
segmentIdx = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle not found (wraparound or timing gaps)
|
||||
if (!found) {
|
||||
segmentIdx = path.looping ? (path.points.size() - 1) :
|
||||
(path.points.size() >= 2 ? path.points.size() - 2 : 0);
|
||||
}
|
||||
|
||||
size_t numPoints = path.points.size();
|
||||
|
||||
// Get 4 control points for Catmull-Rom
|
||||
size_t p0Idx = (segmentIdx == 0) ? (path.looping ? numPoints - 1 : 0) : segmentIdx - 1;
|
||||
|
|
@ -193,17 +284,24 @@ glm::vec3 TransportManager::interpolatePath(const TransportPath& path, size_t se
|
|||
size_t p2Idx = (segmentIdx + 1) % numPoints;
|
||||
size_t p3Idx = (segmentIdx + 2) % numPoints;
|
||||
|
||||
// If non-looping and at boundaries, clamp indices
|
||||
if (!path.looping) {
|
||||
if (segmentIdx == 0) p0Idx = 0;
|
||||
if (segmentIdx >= numPoints - 2) p3Idx = numPoints - 1;
|
||||
if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1;
|
||||
}
|
||||
|
||||
glm::vec3 p0 = path.waypoints[p0Idx];
|
||||
glm::vec3 p1 = path.waypoints[p1Idx];
|
||||
glm::vec3 p2 = path.waypoints[p2Idx];
|
||||
glm::vec3 p3 = path.waypoints[p3Idx];
|
||||
glm::vec3 p0 = path.points[p0Idx].pos;
|
||||
glm::vec3 p1 = path.points[p1Idx].pos;
|
||||
glm::vec3 p2 = path.points[p2Idx].pos;
|
||||
glm::vec3 p3 = path.points[p3Idx].pos;
|
||||
|
||||
// Calculate t (0.0 to 1.0 within segment)
|
||||
// No special case needed - wrap point is explicit in the array now
|
||||
uint32_t t1Ms = path.points[p1Idx].tMs;
|
||||
uint32_t t2Ms = path.points[p2Idx].tMs;
|
||||
uint32_t segmentDurationMs = (t2Ms > t1Ms) ? (t2Ms - t1Ms) : 1;
|
||||
float t = (float)(pathTimeMs - t1Ms) / (float)segmentDurationMs;
|
||||
t = glm::clamp(t, 0.0f, 1.0f);
|
||||
|
||||
// Catmull-Rom spline formula
|
||||
float t2 = t * t;
|
||||
|
|
@ -219,9 +317,33 @@ glm::vec3 TransportManager::interpolatePath(const TransportPath& path, size_t se
|
|||
return result;
|
||||
}
|
||||
|
||||
glm::quat TransportManager::calculateOrientation(const TransportPath& path, size_t segmentIdx, float t) {
|
||||
// Calculate tangent vector for orientation
|
||||
size_t numPoints = path.waypoints.size();
|
||||
glm::quat TransportManager::orientationFromTangent(const TransportPath& path, uint32_t pathTimeMs) {
|
||||
if (path.points.empty()) {
|
||||
return glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
if (path.points.size() == 1) {
|
||||
return glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// Find the segment containing pathTimeMs
|
||||
size_t segmentIdx = 0;
|
||||
bool found = false;
|
||||
|
||||
for (size_t i = 0; i + 1 < path.points.size(); i++) {
|
||||
if (pathTimeMs >= path.points[i].tMs && pathTimeMs < path.points[i + 1].tMs) {
|
||||
segmentIdx = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle not found (wraparound or timing gaps)
|
||||
if (!found) {
|
||||
segmentIdx = path.looping ? (path.points.size() - 1) :
|
||||
(path.points.size() >= 2 ? path.points.size() - 2 : 0);
|
||||
}
|
||||
|
||||
size_t numPoints = path.points.size();
|
||||
|
||||
// Get 4 control points
|
||||
size_t p0Idx = (segmentIdx == 0) ? (path.looping ? numPoints - 1 : 0) : segmentIdx - 1;
|
||||
|
|
@ -235,10 +357,18 @@ glm::quat TransportManager::calculateOrientation(const TransportPath& path, size
|
|||
if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1;
|
||||
}
|
||||
|
||||
glm::vec3 p0 = path.waypoints[p0Idx];
|
||||
glm::vec3 p1 = path.waypoints[p1Idx];
|
||||
glm::vec3 p2 = path.waypoints[p2Idx];
|
||||
glm::vec3 p3 = path.waypoints[p3Idx];
|
||||
glm::vec3 p0 = path.points[p0Idx].pos;
|
||||
glm::vec3 p1 = path.points[p1Idx].pos;
|
||||
glm::vec3 p2 = path.points[p2Idx].pos;
|
||||
glm::vec3 p3 = path.points[p3Idx].pos;
|
||||
|
||||
// Calculate t (0.0 to 1.0 within segment)
|
||||
// No special case needed - wrap point is explicit in the array now
|
||||
uint32_t t1Ms = path.points[p1Idx].tMs;
|
||||
uint32_t t2Ms = path.points[p2Idx].tMs;
|
||||
uint32_t segmentDurationMs = (t2Ms > t1Ms) ? (t2Ms - t1Ms) : 1;
|
||||
float t = (float)(pathTimeMs - t1Ms) / (float)segmentDurationMs;
|
||||
t = glm::clamp(t, 0.0f, 1.0f);
|
||||
|
||||
// Tangent of Catmull-Rom spline (derivative)
|
||||
float t2 = t * t;
|
||||
|
|
@ -263,7 +393,6 @@ glm::quat TransportManager::calculateOrientation(const TransportPath& path, size
|
|||
tangent /= tangentLength;
|
||||
|
||||
// Calculate rotation from forward direction
|
||||
// WoW forward is typically +Y, but we'll use the tangent as forward
|
||||
glm::vec3 forward = tangent;
|
||||
glm::vec3 up(0.0f, 0.0f, 1.0f); // WoW Z is up
|
||||
|
||||
|
|
@ -285,13 +414,341 @@ glm::quat TransportManager::calculateOrientation(const TransportPath& path, size
|
|||
}
|
||||
|
||||
void TransportManager::updateTransformMatrices(ActiveTransport& transport) {
|
||||
// Convert position from canonical to render coordinates for WMO rendering
|
||||
// Canonical: +X=North, +Y=West, +Z=Up
|
||||
// Render: renderX=wowY (west), renderY=wowX (north), renderZ=wowZ (up)
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
||||
|
||||
// Convert rotation from canonical to render space using proper basis change
|
||||
// Canonical → Render is a 90° CCW rotation around Z (swaps X and Y)
|
||||
// Proper formula: q_render = q_basis * q_canonical * q_basis^-1
|
||||
glm::quat basisRotation = glm::angleAxis(glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
glm::quat basisInverse = glm::conjugate(basisRotation);
|
||||
glm::quat renderRot = basisRotation * transport.rotation * basisInverse;
|
||||
|
||||
// Build transform matrix: translate * rotate * scale
|
||||
glm::mat4 translation = glm::translate(glm::mat4(1.0f), transport.position);
|
||||
glm::mat4 rotation = glm::mat4_cast(transport.rotation);
|
||||
glm::mat4 translation = glm::translate(glm::mat4(1.0f), renderPos);
|
||||
glm::mat4 rotation = glm::mat4_cast(renderRot);
|
||||
glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(1.0f)); // No scaling for transports
|
||||
|
||||
transport.transform = translation * rotation * scale;
|
||||
transport.invTransform = glm::inverse(transport.transform);
|
||||
}
|
||||
|
||||
void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& position, float orientation) {
|
||||
auto* transport = getTransport(guid);
|
||||
if (!transport) {
|
||||
LOG_WARNING("TransportManager::updateServerTransport: Transport not found: 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track server updates
|
||||
transport->serverUpdateCount++;
|
||||
transport->lastServerUpdate = elapsedTime_;
|
||||
|
||||
auto pathIt = paths_.find(transport->pathId);
|
||||
if (pathIt == paths_.end() || pathIt->second.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 (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& path = pathIt->second;
|
||||
|
||||
// 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->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
||||
updateTransformMatrices(*transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
}
|
||||
|
||||
bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) {
|
||||
LOG_INFO("Loading TransportAnimation.dbc...");
|
||||
|
||||
if (!assetMgr) {
|
||||
LOG_ERROR("AssetManager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load DBC file
|
||||
auto dbcData = assetMgr->readFile("DBFilesClient\\TransportAnimation.dbc");
|
||||
if (dbcData.empty()) {
|
||||
LOG_WARNING("TransportAnimation.dbc not found - transports will use fallback paths");
|
||||
return false;
|
||||
}
|
||||
|
||||
pipeline::DBCFile dbc;
|
||||
if (!dbc.load(dbcData)) {
|
||||
LOG_ERROR("Failed to parse TransportAnimation.dbc");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("TransportAnimation.dbc: ", dbc.getRecordCount(), " records, ",
|
||||
dbc.getFieldCount(), " fields per record");
|
||||
|
||||
// Debug: dump first 3 records to see all field values
|
||||
for (uint32_t i = 0; i < std::min(3u, dbc.getRecordCount()); i++) {
|
||||
LOG_INFO(" DEBUG Record ", i, ": ",
|
||||
" [0]=", dbc.getUInt32(i, 0),
|
||||
" [1]=", dbc.getUInt32(i, 1),
|
||||
" [2]=", dbc.getUInt32(i, 2),
|
||||
" [3]=", dbc.getFloat(i, 3),
|
||||
" [4]=", dbc.getFloat(i, 4),
|
||||
" [5]=", dbc.getFloat(i, 5),
|
||||
" [6]=", dbc.getUInt32(i, 6));
|
||||
}
|
||||
|
||||
// Group waypoints by transportEntry
|
||||
std::map<uint32_t, std::vector<std::pair<uint32_t, glm::vec3>>> waypointsByTransport;
|
||||
|
||||
for (uint32_t i = 0; i < dbc.getRecordCount(); i++) {
|
||||
// uint32_t id = dbc.getUInt32(i, 0); // Not needed
|
||||
uint32_t transportEntry = dbc.getUInt32(i, 1);
|
||||
uint32_t timeIndex = dbc.getUInt32(i, 2);
|
||||
float posX = dbc.getFloat(i, 3);
|
||||
float posY = dbc.getFloat(i, 4);
|
||||
float posZ = dbc.getFloat(i, 5);
|
||||
// uint32_t sequenceId = dbc.getUInt32(i, 6); // Not needed for basic paths
|
||||
|
||||
// RAW FLOAT SANITY CHECK: Log first 10 records to see if DBC has real data
|
||||
if (i < 10) {
|
||||
uint32_t ux = dbc.getUInt32(i, 3);
|
||||
uint32_t uy = dbc.getUInt32(i, 4);
|
||||
uint32_t uz = dbc.getUInt32(i, 5);
|
||||
LOG_INFO("TA raw rec ", i,
|
||||
" entry=", transportEntry,
|
||||
" t=", timeIndex,
|
||||
" raw=(", posX, ",", posY, ",", posZ, ")",
|
||||
" u32=(", ux, ",", uy, ",", uz, ")");
|
||||
}
|
||||
|
||||
waypointsByTransport[transportEntry].push_back({timeIndex, glm::vec3(posX, posY, posZ)});
|
||||
}
|
||||
|
||||
// Create time-indexed paths from waypoints
|
||||
int pathsLoaded = 0;
|
||||
for (const auto& [transportEntry, waypoints] : waypointsByTransport) {
|
||||
if (waypoints.empty()) continue;
|
||||
|
||||
// Sort by timeIndex
|
||||
auto sortedWaypoints = waypoints;
|
||||
std::sort(sortedWaypoints.begin(), sortedWaypoints.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
|
||||
// CRITICAL: Normalize timeIndex to start at 0 (DBC records don't start at 0!)
|
||||
// This makes evalTimedCatmullRom(path, 0) valid and stabilizes basePosition seeding
|
||||
uint32_t t0 = sortedWaypoints.front().first;
|
||||
|
||||
// Build TimedPoint array with normalized time indices
|
||||
std::vector<TimedPoint> timedPoints;
|
||||
timedPoints.reserve(sortedWaypoints.size() + 1); // +1 for wrap point
|
||||
|
||||
// Log first few waypoints for transport 2074 to see conversion
|
||||
for (size_t idx = 0; idx < sortedWaypoints.size(); idx++) {
|
||||
const auto& [tMs, pos] = sortedWaypoints[idx];
|
||||
|
||||
// TransportAnimation.dbc uses server coordinates - convert to canonical
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(pos);
|
||||
|
||||
// Debug waypoint conversion for first transport (entry 2074)
|
||||
if (transportEntry == 2074 && idx < 5) {
|
||||
LOG_INFO("COORD CONVERT: entry=", transportEntry, " t=", tMs,
|
||||
" serverPos=(", pos.x, ", ", pos.y, ", ", pos.z, ")",
|
||||
" → canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ")");
|
||||
}
|
||||
|
||||
timedPoints.push_back({tMs - t0, canonical}); // Normalize: subtract first timeIndex
|
||||
}
|
||||
|
||||
// Get base duration from last normalized timeIndex
|
||||
uint32_t lastTimeMs = sortedWaypoints.back().first - t0;
|
||||
|
||||
// Calculate wrap duration (last → first segment)
|
||||
// Use average segment duration as wrap duration
|
||||
uint32_t totalDelta = 0;
|
||||
int segmentCount = 0;
|
||||
for (size_t i = 1; i < sortedWaypoints.size(); i++) {
|
||||
uint32_t delta = sortedWaypoints[i].first - sortedWaypoints[i-1].first;
|
||||
if (delta > 0) {
|
||||
totalDelta += delta;
|
||||
segmentCount++;
|
||||
}
|
||||
}
|
||||
uint32_t wrapMs = (segmentCount > 0) ? (totalDelta / segmentCount) : 1000;
|
||||
|
||||
// Add duplicate first point at end with wrap duration
|
||||
// This makes the wrap segment (last → first) have proper duration
|
||||
glm::vec3 firstCanonical = core::coords::serverToCanonical(sortedWaypoints.front().second);
|
||||
timedPoints.push_back({lastTimeMs + wrapMs, firstCanonical});
|
||||
|
||||
uint32_t durationMs = lastTimeMs + wrapMs;
|
||||
|
||||
// Store path
|
||||
TransportPath path;
|
||||
path.pathId = transportEntry;
|
||||
path.points = timedPoints;
|
||||
// Keep looping=true even with duplicate wrap point for smooth control point selection at seam
|
||||
// This prevents kinks on the last segment approaching the wrap
|
||||
path.looping = true;
|
||||
path.durationMs = durationMs;
|
||||
paths_[transportEntry] = path;
|
||||
pathsLoaded++;
|
||||
|
||||
// Log first, middle, and last points to verify path data
|
||||
glm::vec3 firstOffset = timedPoints[0].pos;
|
||||
size_t midIdx = timedPoints.size() / 2;
|
||||
glm::vec3 midOffset = timedPoints[midIdx].pos;
|
||||
glm::vec3 lastOffset = timedPoints[timedPoints.size() - 2].pos; // -2 to skip wrap duplicate
|
||||
LOG_INFO(" Transport ", transportEntry, ": ", timedPoints.size() - 1, " waypoints + wrap, ",
|
||||
durationMs, "ms duration (wrap=", wrapMs, "ms, t0_normalized=", timedPoints[0].tMs, "ms)",
|
||||
" firstOffset=(", firstOffset.x, ", ", firstOffset.y, ", ", firstOffset.z, ")",
|
||||
" midOffset=(", midOffset.x, ", ", midOffset.y, ", ", midOffset.z, ")",
|
||||
" lastOffset=(", lastOffset.x, ", ", lastOffset.y, ", ", lastOffset.z, ")");
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", pathsLoaded, " transport paths from TransportAnimation.dbc");
|
||||
return pathsLoaded > 0;
|
||||
}
|
||||
|
||||
bool TransportManager::hasPathForEntry(uint32_t entry) const {
|
||||
return paths_.find(entry) != paths_.end();
|
||||
}
|
||||
|
||||
} // namespace wowee::game
|
||||
|
|
|
|||
|
|
@ -634,6 +634,15 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec);
|
||||
|
||||
// Log transport-related flag combinations
|
||||
if (updateFlags & 0x0002) { // UPDATEFLAG_TRANSPORT
|
||||
LOG_INFO(" Transport flags detected: 0x", std::hex, updateFlags, std::dec,
|
||||
" (TRANSPORT=", !!(updateFlags & 0x0002),
|
||||
", POSITION=", !!(updateFlags & 0x0100),
|
||||
", ROTATION=", !!(updateFlags & 0x0200),
|
||||
", STATIONARY=", !!(updateFlags & 0x0040), ")");
|
||||
}
|
||||
|
||||
// UpdateFlags bit meanings:
|
||||
// 0x0001 = UPDATEFLAG_SELF
|
||||
// 0x0002 = UPDATEFLAG_TRANSPORT
|
||||
|
|
@ -771,8 +780,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_POSITION) {
|
||||
// Transport position update
|
||||
/*uint64_t transportGuid =*/ readPackedGuid(packet);
|
||||
// Transport position update (UPDATEFLAG_POSITION = 0x0100)
|
||||
uint64_t transportGuid = readPackedGuid(packet);
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
|
|
@ -783,7 +792,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
/*float corpseOrientation =*/ packet.readFloat();
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_DEBUG(" POSITION: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation);
|
||||
LOG_INFO(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec,
|
||||
" pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation);
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) {
|
||||
// Simple stationary position (4 floats)
|
||||
|
|
|
|||
|
|
@ -2505,6 +2505,28 @@ void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& posit
|
|||
rebuildSpatialIndex();
|
||||
}
|
||||
|
||||
void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return;
|
||||
auto& inst = instances[idxIt->second];
|
||||
|
||||
// Update model matrix directly
|
||||
inst.modelMatrix = transform;
|
||||
inst.invModelMatrix = glm::inverse(transform);
|
||||
|
||||
// Extract position from transform for bounds
|
||||
inst.position = glm::vec3(transform[3]);
|
||||
|
||||
// Update bounds
|
||||
auto modelIt = models.find(inst.modelId);
|
||||
if (modelIt != models.end()) {
|
||||
glm::vec3 localMin, localMax;
|
||||
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
||||
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
||||
}
|
||||
rebuildSpatialIndex();
|
||||
}
|
||||
|
||||
void M2Renderer::removeInstance(uint32_t instanceId) {
|
||||
for (auto it = instances.begin(); it != instances.end(); ++it) {
|
||||
if (it->id == instanceId) {
|
||||
|
|
|
|||
|
|
@ -610,13 +610,29 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) {
|
|||
bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0;
|
||||
renderTile = lsbOrder || msbOrder;
|
||||
|
||||
// If this tile is masked out, check neighbors to fill gaps
|
||||
if (!renderTile && x > 0 && y > 0 && x < gridWidth-2 && y < gridHeight-2) {
|
||||
// If this tile is masked out, check neighbors to fill coastline gaps
|
||||
if (!renderTile) {
|
||||
// Check adjacent tiles - render if any neighbor is water (blend coastline)
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
if (dx == 0 && dy == 0) continue;
|
||||
int neighborIdx = (y + dy) * surface.width + (x + dx);
|
||||
int nx = x + dx;
|
||||
int ny = y + dy;
|
||||
// Bounds check neighbors
|
||||
if (nx < 0 || ny < 0 || nx >= gridWidth-1 || ny >= gridHeight-1) continue;
|
||||
|
||||
// Calculate neighbor mask index (consistent with main tile indexing)
|
||||
int neighborIdx;
|
||||
if (surface.wmoId == 0 && surface.mask.size() >= 8) {
|
||||
// Terrain MH2O: account for xOffset/yOffset
|
||||
int ncx = static_cast<int>(surface.xOffset) + nx;
|
||||
int ncy = static_cast<int>(surface.yOffset) + ny;
|
||||
neighborIdx = ncy * 8 + ncx;
|
||||
} else {
|
||||
// WMO/custom: local indexing
|
||||
neighborIdx = ny * surface.width + nx;
|
||||
}
|
||||
|
||||
int nByteIdx = neighborIdx / 8;
|
||||
int nBitIdx = neighborIdx % 8;
|
||||
if (nByteIdx < static_cast<int>(surface.mask.size())) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue