mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add transport registration to movement packets (WIP - awaiting server MOVEMENT updates)
- Added transport fields to MovementInfo struct (transportGuid, transportX/Y/Z/O, transportTime) - Updated MovementPacket::build() to serialize transport data when ONTRANSPORT flag set - Modified GameHandler::sendMovement() to include transport info when player on transport - Fixed coordinate conversion for transport offsets (server↔canonical) - Added transport tracking in both CREATE_OBJECT and MOVEMENT update handlers - Connected M2Renderer to WMORenderer for hierarchical doodad transforms - Server-authoritative transport movement (no client-side animation) Issue: Server not sending MOVEMENT updates for transports, so they remain stationary. Transports register successfully but don't animate without server position updates.
This commit is contained in:
parent
f3f3b62880
commit
55a40fc3aa
8 changed files with 425 additions and 60 deletions
|
|
@ -27,6 +27,7 @@ struct TransportPath {
|
|||
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)
|
||||
bool zOnly; // True if path only has Z movement (elevator/bobbing), false if real XY travel
|
||||
};
|
||||
|
||||
struct ActiveTransport {
|
||||
|
|
@ -100,7 +101,7 @@ private:
|
|||
std::unordered_map<uint64_t, ActiveTransport> transports_;
|
||||
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
|
||||
bool clientSideAnimation_ = false; // DISABLED - use server positions instead of client prediction
|
||||
float elapsedTime_ = 0.0f; // Total elapsed time (seconds)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -411,6 +411,14 @@ struct MovementInfo {
|
|||
float jumpCosAngle = 0.0f; // Jump horizontal cos
|
||||
float jumpXYSpeed = 0.0f; // Jump horizontal speed
|
||||
|
||||
// Transport fields (when ONTRANSPORT flag is set)
|
||||
uint64_t transportGuid = 0; // GUID of transport (boat/zeppelin/etc)
|
||||
float transportX = 0.0f; // Local position on transport
|
||||
float transportY = 0.0f;
|
||||
float transportZ = 0.0f;
|
||||
float transportO = 0.0f; // Local orientation on transport
|
||||
uint32_t transportTime = 0; // Transport movement timestamp
|
||||
|
||||
bool hasFlag(MovementFlags flag) const {
|
||||
return (flags & static_cast<uint32_t>(flag)) != 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace rendering {
|
|||
class Camera;
|
||||
class Shader;
|
||||
class Frustum;
|
||||
class M2Renderer;
|
||||
|
||||
/**
|
||||
* WMO (World Model Object) Renderer
|
||||
|
|
@ -49,6 +50,11 @@ public:
|
|||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* Set M2 renderer for hierarchical transform updates (doodads follow parent WMO)
|
||||
*/
|
||||
void setM2Renderer(M2Renderer* renderer) { m2Renderer_ = renderer; }
|
||||
|
||||
/**
|
||||
* Load WMO model and create GPU resources
|
||||
* @param model WMO model with geometry data
|
||||
|
|
@ -89,6 +95,27 @@ public:
|
|||
*/
|
||||
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
||||
|
||||
/**
|
||||
* Add doodad (child M2) to WMO instance
|
||||
* @param instanceId WMO instance to add doodad to
|
||||
* @param m2InstanceId M2 instance ID of the doodad
|
||||
* @param localTransform Local transform relative to WMO origin
|
||||
*/
|
||||
void addDoodadToInstance(uint32_t instanceId, uint32_t m2InstanceId, const glm::mat4& localTransform);
|
||||
|
||||
// Forward declare DoodadTemplate for public API
|
||||
struct DoodadTemplate {
|
||||
std::string m2Path;
|
||||
glm::mat4 localTransform;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get doodad templates for a WMO model
|
||||
* @param modelId WMO model ID
|
||||
* @return Vector of doodad templates (empty if no doodads or model not found)
|
||||
*/
|
||||
const std::vector<DoodadTemplate>* getDoodadTemplates(uint32_t modelId) const;
|
||||
|
||||
/**
|
||||
* Remove WMO instance
|
||||
* @param instanceId Instance to remove
|
||||
|
|
@ -363,6 +390,10 @@ private:
|
|||
glm::vec3 boundingBoxMax;
|
||||
bool isLowPlatform = false;
|
||||
|
||||
// Doodad templates (M2 models placed in WMO, stored for instancing)
|
||||
// Uses the public DoodadTemplate struct defined above
|
||||
std::vector<DoodadTemplate> doodadTemplates;
|
||||
|
||||
// Texture handles for this model (indexed by texture path order)
|
||||
std::vector<GLuint> textures;
|
||||
|
||||
|
|
@ -510,6 +541,9 @@ private:
|
|||
// Asset manager for loading textures
|
||||
pipeline::AssetManager* assetManager = nullptr;
|
||||
|
||||
// M2 renderer for hierarchical transforms (doodads following WMO parent)
|
||||
M2Renderer* m2Renderer_ = nullptr;
|
||||
|
||||
// Current map name for zone-specific floor cache
|
||||
std::string mapName_;
|
||||
|
||||
|
|
|
|||
|
|
@ -903,16 +903,10 @@ void Application::setupUICallbacks() {
|
|||
// 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");
|
||||
}
|
||||
// Server-authoritative movement - set initial position from spawn data
|
||||
glm::vec3 canonicalPos(x, y, z);
|
||||
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
||||
LOG_INFO("Transport registered - server-authoritative movement");
|
||||
});
|
||||
|
||||
// Transport move callback (online mode) - update transport gameobject positions
|
||||
|
|
@ -1792,6 +1786,12 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
LOG_INFO("TransportManager connected to WMORenderer for online mode");
|
||||
}
|
||||
|
||||
// Connect WMORenderer to M2Renderer (for hierarchical transforms: doodads following WMO parents)
|
||||
if (renderer->getWMORenderer() && renderer->getM2Renderer()) {
|
||||
renderer->getWMORenderer()->setM2Renderer(renderer->getM2Renderer());
|
||||
LOG_INFO("WMORenderer connected to M2Renderer for hierarchical doodad transforms");
|
||||
}
|
||||
|
||||
showProgress("Loading character model...", 0.05f);
|
||||
|
||||
// Build faction hostility map for this character's race
|
||||
|
|
@ -2870,6 +2870,65 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
|
||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
|
||||
// Spawn WMO doodads (chairs, furniture, etc.) as child M2 instances
|
||||
// TODO: Re-enable after implementing deferred/background loading
|
||||
// Currently disabled - spawning 134 doodads synchronously causes massive slowdown
|
||||
bool isTransport = false;
|
||||
if (gameHandler) {
|
||||
std::string lowerModelPath = modelPath;
|
||||
std::transform(lowerModelPath.begin(), lowerModelPath.end(), lowerModelPath.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
isTransport = (lowerModelPath.find("transport") != std::string::npos);
|
||||
}
|
||||
|
||||
auto* m2Renderer = renderer->getM2Renderer();
|
||||
if (false && m2Renderer && isTransport) { // DISABLED - too slow
|
||||
const auto* doodadTemplates = wmoRenderer->getDoodadTemplates(modelId);
|
||||
if (doodadTemplates && !doodadTemplates->empty()) {
|
||||
LOG_INFO("Spawning ", doodadTemplates->size(), " doodads for transport WMO instance ", instanceId);
|
||||
int spawnedDoodads = 0;
|
||||
|
||||
for (const auto& doodadTemplate : *doodadTemplates) {
|
||||
// Load M2 model (may be cached)
|
||||
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(doodadTemplate.m2Path));
|
||||
auto m2Data = assetManager->readFile(doodadTemplate.m2Path);
|
||||
if (m2Data.empty()) continue;
|
||||
|
||||
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
||||
std::string skinPath = doodadTemplate.m2Path.substr(0, doodadTemplate.m2Path.size() - 3) + "00.skin";
|
||||
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
||||
}
|
||||
if (!m2Model.isValid()) continue;
|
||||
|
||||
// Load model to renderer (cached if already loaded)
|
||||
m2Renderer->loadModel(m2Model, doodadModelId);
|
||||
|
||||
// Create M2 instance at world origin (transform will be updated by WMO parent)
|
||||
uint32_t m2InstanceId = m2Renderer->createInstance(doodadModelId, glm::vec3(0.0f), glm::vec3(0.0f), 1.0f);
|
||||
if (m2InstanceId == 0) continue;
|
||||
|
||||
// Link doodad to WMO instance
|
||||
wmoRenderer->addDoodadToInstance(instanceId, m2InstanceId, doodadTemplate.localTransform);
|
||||
spawnedDoodads++;
|
||||
}
|
||||
|
||||
if (spawnedDoodads > 0) {
|
||||
LOG_INFO("Spawned ", spawnedDoodads, " doodads for transport WMO instance ", instanceId);
|
||||
|
||||
// Initial transform update to position doodads correctly
|
||||
// (subsequent updates will happen automatically via setInstanceTransform)
|
||||
glm::mat4 wmoTransform(1.0f);
|
||||
wmoTransform = glm::translate(wmoTransform, renderPos);
|
||||
wmoTransform = glm::rotate(wmoTransform, renderYaw, glm::vec3(0, 0, 1));
|
||||
wmoRenderer->setInstanceTransform(instanceId, wmoTransform);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Transport WMO has no doodads or templates not available");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a transport and notify via special method
|
||||
if (gameHandler) {
|
||||
std::string lowerModelPath = modelPath;
|
||||
|
|
@ -3285,6 +3344,12 @@ void Application::setupTestTransport() {
|
|||
// Connect transport manager to WMO renderer
|
||||
transportManager->setWMORenderer(wmoRenderer);
|
||||
|
||||
// Connect WMORenderer to M2Renderer (for hierarchical transforms: doodads following WMO parents)
|
||||
if (renderer->getM2Renderer()) {
|
||||
wmoRenderer->setM2Renderer(renderer->getM2Renderer());
|
||||
LOG_INFO("WMORenderer connected to M2Renderer for test transport doodad transforms");
|
||||
}
|
||||
|
||||
// Define a simple circular path around Stormwind harbor (canonical coordinates)
|
||||
// These coordinates are approximate - adjust based on actual harbor layout
|
||||
std::vector<glm::vec3> harborPath = {
|
||||
|
|
|
|||
|
|
@ -1775,8 +1775,24 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Add transport data if player is on a transport
|
||||
if (isOnTransport()) {
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
||||
movementInfo.transportGuid = playerTransportGuid_;
|
||||
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
|
||||
} else {
|
||||
// Clear transport flag if not on transport
|
||||
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
||||
movementInfo.transportGuid = 0;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
|
||||
static_cast<uint16_t>(opcode), std::dec);
|
||||
static_cast<uint16_t>(opcode), std::dec,
|
||||
(isOnTransport() ? " ONTRANSPORT" : ""));
|
||||
|
||||
// Convert canonical → server coordinates for the wire
|
||||
MovementInfo wireInfo = movementInfo;
|
||||
|
|
@ -1785,6 +1801,15 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
wireInfo.y = serverPos.y;
|
||||
wireInfo.z = serverPos.z;
|
||||
|
||||
// Also convert transport local position to server coordinates if on transport
|
||||
if (isOnTransport()) {
|
||||
glm::vec3 serverTransportPos = core::coords::canonicalToServer(
|
||||
glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ));
|
||||
wireInfo.transportX = serverTransportPos.x;
|
||||
wireInfo.transportY = serverTransportPos.y;
|
||||
wireInfo.transportZ = serverTransportPos.z;
|
||||
}
|
||||
|
||||
// Build and send movement packet
|
||||
auto packet = MovementPacket::build(opcode, wireInfo, playerGuid);
|
||||
socket->send(packet);
|
||||
|
|
@ -1869,7 +1894,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (block.guid == playerGuid) {
|
||||
if (block.onTransport) {
|
||||
playerTransportGuid_ = block.transportGuid;
|
||||
playerTransportOffset_ = glm::vec3(block.transportX, block.transportY, block.transportZ);
|
||||
// Convert transport offset from server → canonical coordinates
|
||||
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
|
||||
playerTransportOffset_ = core::coords::serverToCanonical(serverOffset);
|
||||
LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec,
|
||||
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
|
||||
} else {
|
||||
|
|
@ -2359,6 +2386,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
movementInfo.y = pos.y;
|
||||
movementInfo.z = pos.z;
|
||||
movementInfo.orientation = block.orientation;
|
||||
|
||||
// Track player-on-transport state from MOVEMENT updates
|
||||
if (block.onTransport) {
|
||||
playerTransportGuid_ = block.transportGuid;
|
||||
// Convert transport offset from server → canonical coordinates
|
||||
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
|
||||
playerTransportOffset_ = core::coords::serverToCanonical(serverOffset);
|
||||
LOG_INFO("Player on transport (MOVEMENT): 0x", std::hex, playerTransportGuid_, std::dec);
|
||||
} else {
|
||||
if (playerTransportGuid_ != 0) {
|
||||
LOG_INFO("Player left transport (MOVEMENT)");
|
||||
playerTransportGuid_ = 0;
|
||||
playerTransportOffset_ = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fire transport move callback if this is a known transport
|
||||
|
|
|
|||
|
|
@ -55,6 +55,16 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
glm::vec3 offset0 = evalTimedCatmullRom(path, 0);
|
||||
transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn
|
||||
transport.position = spawnWorldPos; // Start at spawn position (base + offset0)
|
||||
|
||||
// Sanity check: firstWaypoint should match spawnWorldPos
|
||||
glm::vec3 firstWaypoint = path.points[0].pos;
|
||||
glm::vec3 waypointDiff = spawnWorldPos - firstWaypoint;
|
||||
if (glm::length(waypointDiff) > 1.0f) {
|
||||
LOG_WARNING("Transport 0x", std::hex, guid, std::dec, " path ", pathId,
|
||||
": firstWaypoint mismatch! spawnPos=(", spawnWorldPos.x, ",", spawnWorldPos.y, ",", spawnWorldPos.z, ")",
|
||||
" firstWaypoint=(", firstWaypoint.x, ",", firstWaypoint.y, ",", firstWaypoint.z, ")",
|
||||
" diff=(", waypointDiff.x, ",", waypointDiff.y, ",", waypointDiff.z, ")");
|
||||
}
|
||||
}
|
||||
|
||||
transport.rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion
|
||||
|
|
@ -64,7 +74,8 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.localClockMs = 0;
|
||||
transport.hasServerClock = false;
|
||||
transport.serverClockOffsetMs = 0;
|
||||
transport.useClientAnimation = clientSideAnimation_; // Enable client animation by default
|
||||
// Server-authoritative movement only - no client-side animation
|
||||
transport.useClientAnimation = false;
|
||||
transport.serverYaw = 0.0f;
|
||||
transport.hasServerYaw = false;
|
||||
transport.lastServerUpdate = 0.0f;
|
||||
|
|
@ -128,33 +139,53 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
|
|||
|
||||
TransportPath path;
|
||||
path.pathId = pathId;
|
||||
path.looping = looping;
|
||||
path.zOnly = false; // Manually loaded paths are assumed to have XY movement
|
||||
|
||||
// Helper: compute segment duration from distance and speed
|
||||
auto segMsFromDist = [&](float dist) -> uint32_t {
|
||||
if (speed <= 0.0f) return 1000;
|
||||
return (uint32_t)((dist / speed) * 1000.0f);
|
||||
};
|
||||
|
||||
// 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;
|
||||
path.durationMs = 0;
|
||||
path.looping = false;
|
||||
paths_[pathId] = path;
|
||||
LOG_INFO("TransportManager: Loaded stationary path ", pathId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple points: calculate cumulative time based on distance and speed
|
||||
path.points.reserve(waypoints.size() + (looping ? 1 : 0));
|
||||
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]);
|
||||
cumulativeMs += glm::max(1u, segMsFromDist(dist));
|
||||
path.points.push_back({cumulativeMs, waypoints[i]});
|
||||
}
|
||||
|
||||
// Add explicit wrap segment (last → first) for looping paths
|
||||
if (looping) {
|
||||
float wrapDist = glm::distance(waypoints.back(), waypoints.front());
|
||||
uint32_t wrapMs = glm::max(1u, segMsFromDist(wrapDist));
|
||||
cumulativeMs += wrapMs;
|
||||
path.points.push_back({cumulativeMs, waypoints.front()}); // Duplicate first point
|
||||
path.looping = false; // Time-closed path, no need for index wrapping
|
||||
} else {
|
||||
path.looping = false;
|
||||
}
|
||||
|
||||
path.durationMs = cumulativeMs;
|
||||
paths_[pathId] = path;
|
||||
|
||||
LOG_INFO("TransportManager: Loaded path ", pathId,
|
||||
" with ", waypoints.size(), " waypoints, "
|
||||
"looping=", looping, ", speed=", speed);
|
||||
" with ", waypoints.size(), " waypoints",
|
||||
(looping ? " + wrap segment" : ""),
|
||||
", duration=", path.durationMs, "ms, speed=", speed);
|
||||
}
|
||||
|
||||
void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) {
|
||||
|
|
@ -270,24 +301,34 @@ glm::vec3 TransportManager::evalTimedCatmullRom(const TransportPath& path, uint3
|
|||
}
|
||||
}
|
||||
|
||||
// Handle not found (wraparound or timing gaps)
|
||||
// Handle not found (timing gaps or past last segment)
|
||||
if (!found) {
|
||||
segmentIdx = path.looping ? (path.points.size() - 1) :
|
||||
(path.points.size() >= 2 ? path.points.size() - 2 : 0);
|
||||
// For time-closed paths (explicit wrap point), last valid segment is points.size() - 2
|
||||
segmentIdx = (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;
|
||||
size_t p1Idx = segmentIdx;
|
||||
size_t p2Idx = (segmentIdx + 1) % numPoints;
|
||||
size_t p3Idx = (segmentIdx + 2) % numPoints;
|
||||
// Helper to clamp index (no wrapping for non-looping paths)
|
||||
auto idxClamp = [&](size_t i) -> size_t {
|
||||
return (i >= numPoints) ? (numPoints - 1) : i;
|
||||
};
|
||||
|
||||
if (!path.looping) {
|
||||
if (segmentIdx == 0) p0Idx = 0;
|
||||
if (segmentIdx >= numPoints - 2) p3Idx = numPoints - 1;
|
||||
if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1;
|
||||
size_t p0Idx, p1Idx, p2Idx, p3Idx;
|
||||
p1Idx = segmentIdx;
|
||||
|
||||
if (path.looping) {
|
||||
// Index-wrapped path (old DBC style with looping=true)
|
||||
p0Idx = (segmentIdx == 0) ? (numPoints - 1) : (segmentIdx - 1);
|
||||
p2Idx = (segmentIdx + 1) % numPoints;
|
||||
p3Idx = (segmentIdx + 2) % numPoints;
|
||||
} else {
|
||||
// Time-closed path (explicit wrap point at end, looping=false)
|
||||
// No index wrapping - points are sequential with possible duplicate at end
|
||||
p0Idx = (segmentIdx == 0) ? 0 : (segmentIdx - 1);
|
||||
p2Idx = idxClamp(segmentIdx + 1);
|
||||
p3Idx = idxClamp(segmentIdx + 2);
|
||||
}
|
||||
|
||||
glm::vec3 p0 = path.points[p0Idx].pos;
|
||||
|
|
@ -337,24 +378,34 @@ glm::quat TransportManager::orientationFromTangent(const TransportPath& path, ui
|
|||
}
|
||||
}
|
||||
|
||||
// Handle not found (wraparound or timing gaps)
|
||||
// Handle not found (timing gaps or past last segment)
|
||||
if (!found) {
|
||||
segmentIdx = path.looping ? (path.points.size() - 1) :
|
||||
(path.points.size() >= 2 ? path.points.size() - 2 : 0);
|
||||
// For time-closed paths (explicit wrap point), last valid segment is points.size() - 2
|
||||
segmentIdx = (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;
|
||||
size_t p1Idx = segmentIdx;
|
||||
size_t p2Idx = (segmentIdx + 1) % numPoints;
|
||||
size_t p3Idx = (segmentIdx + 2) % numPoints;
|
||||
// Get 4 control points for tangent calculation
|
||||
// Helper to clamp index (no wrapping for non-looping paths)
|
||||
auto idxClamp = [&](size_t i) -> size_t {
|
||||
return (i >= numPoints) ? (numPoints - 1) : i;
|
||||
};
|
||||
|
||||
if (!path.looping) {
|
||||
if (segmentIdx == 0) p0Idx = 0;
|
||||
if (segmentIdx >= numPoints - 2) p3Idx = numPoints - 1;
|
||||
if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1;
|
||||
size_t p0Idx, p1Idx, p2Idx, p3Idx;
|
||||
p1Idx = segmentIdx;
|
||||
|
||||
if (path.looping) {
|
||||
// Index-wrapped path (old DBC style with looping=true)
|
||||
p0Idx = (segmentIdx == 0) ? (numPoints - 1) : (segmentIdx - 1);
|
||||
p2Idx = (segmentIdx + 1) % numPoints;
|
||||
p3Idx = (segmentIdx + 2) % numPoints;
|
||||
} else {
|
||||
// Time-closed path (explicit wrap point at end, looping=false)
|
||||
// No index wrapping - points are sequential with possible duplicate at end
|
||||
p0Idx = (segmentIdx == 0) ? 0 : (segmentIdx - 1);
|
||||
p2Idx = idxClamp(segmentIdx + 1);
|
||||
p3Idx = idxClamp(segmentIdx + 2);
|
||||
}
|
||||
|
||||
glm::vec3 p0 = path.points[p0Idx].pos;
|
||||
|
|
@ -461,6 +512,24 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
|
||||
const auto& path = pathIt->second;
|
||||
|
||||
// Z-only paths (elevator/bobbing): server is authoritative, no projection needed
|
||||
if (path.zOnly) {
|
||||
transport->position = position;
|
||||
transport->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
transport->useClientAnimation = false; // Server-driven
|
||||
|
||||
updateTransformMatrices(*transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
|
||||
LOG_INFO("TransportManager: Z-only transport 0x", std::hex, guid, std::dec,
|
||||
" updated from server: pos=(", position.x, ", ", position.y, ", ", position.z, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// Seed basePosition from t=0 assumption before first search
|
||||
// (t=0 corresponds to spawn point / first path point)
|
||||
if (!transport->hasServerClock) {
|
||||
|
|
@ -659,6 +728,14 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
" u32=(", ux, ",", uy, ",", uz, ")");
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Log ALL records for problematic ferries (20655, 20657, 149046)
|
||||
// AND first few records for known-good transports to verify DBC reading
|
||||
if (i < 5 || transportEntry == 2074 ||
|
||||
transportEntry == 20655 || transportEntry == 20657 || transportEntry == 149046) {
|
||||
LOG_INFO("RAW DBC [", i, "] entry=", transportEntry, " t=", timeIndex,
|
||||
" raw=(", posX, ",", posY, ",", posZ, ")");
|
||||
}
|
||||
|
||||
waypointsByTransport[transportEntry].push_back({timeIndex, glm::vec3(posX, posY, posZ)});
|
||||
}
|
||||
|
||||
|
|
@ -687,6 +764,14 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
// TransportAnimation.dbc uses server coordinates - convert to canonical
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(pos);
|
||||
|
||||
// CRITICAL: Detect if serverToCanonical is zeroing nonzero inputs
|
||||
if ((pos.x != 0.0f || pos.y != 0.0f || pos.z != 0.0f) &&
|
||||
(canonical.x == 0.0f && canonical.y == 0.0f && canonical.z == 0.0f)) {
|
||||
LOG_ERROR("serverToCanonical ZEROED! entry=", transportEntry,
|
||||
" server=(", pos.x, ",", pos.y, ",", pos.z, ")",
|
||||
" → canon=(", canonical.x, ",", canonical.y, ",", canonical.z, ")");
|
||||
}
|
||||
|
||||
// Debug waypoint conversion for first transport (entry 2074)
|
||||
if (transportEntry == 2074 && idx < 5) {
|
||||
LOG_INFO("COORD CONVERT: entry=", transportEntry, " t=", tMs,
|
||||
|
|
@ -694,6 +779,13 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
" → canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ")");
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Log ALL conversions for problematic ferries
|
||||
if (transportEntry == 20655 || transportEntry == 20657 || transportEntry == 149046) {
|
||||
LOG_INFO("CONVERT ", transportEntry, " t=", tMs,
|
||||
" server=(", pos.x, ",", pos.y, ",", pos.z, ")",
|
||||
" → canon=(", canonical.x, ",", canonical.y, ",", canonical.z, ")");
|
||||
}
|
||||
|
||||
timedPoints.push_back({tMs - t0, canonical}); // Normalize: subtract first timeIndex
|
||||
}
|
||||
|
||||
|
|
@ -720,14 +812,30 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
|
||||
uint32_t durationMs = lastTimeMs + wrapMs;
|
||||
|
||||
// Detect Z-only paths (elevator/bobbing animation, not real XY travel)
|
||||
float minX = timedPoints[0].pos.x;
|
||||
float maxX = timedPoints[0].pos.x;
|
||||
float minY = timedPoints[0].pos.y;
|
||||
float maxY = timedPoints[0].pos.y;
|
||||
for (const auto& pt : timedPoints) {
|
||||
minX = std::min(minX, pt.pos.x);
|
||||
maxX = std::max(maxX, pt.pos.x);
|
||||
minY = std::min(minY, pt.pos.y);
|
||||
maxY = std::max(maxY, pt.pos.y);
|
||||
}
|
||||
float rangeX = maxX - minX;
|
||||
float rangeY = maxY - minY;
|
||||
bool isZOnly = (rangeX < 0.01f && rangeY < 0.01f);
|
||||
|
||||
// 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;
|
||||
// CRITICAL: We added an explicit wrap point (last → first), so this is TIME-CLOSED, not index-wrapped
|
||||
// Setting looping=false ensures evalTimedCatmullRom uses clamp logic (not modulo) for control points
|
||||
path.looping = false;
|
||||
path.durationMs = durationMs;
|
||||
path.zOnly = isZOnly;
|
||||
paths_[transportEntry] = path;
|
||||
pathsLoaded++;
|
||||
|
||||
|
|
@ -738,6 +846,7 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
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)",
|
||||
" rangeXY=(", rangeX, ",", rangeY, ") ", (isZOnly ? "[Z-ONLY]" : "[XY-PATH]"),
|
||||
" firstOffset=(", firstOffset.x, ", ", firstOffset.y, ", ", firstOffset.z, ")",
|
||||
" midOffset=(", midOffset.x, ", ", midOffset.y, ", ", midOffset.z, ")",
|
||||
" lastOffset=(", lastOffset.x, ", ", lastOffset.y, ", ", lastOffset.z, ")");
|
||||
|
|
|
|||
|
|
@ -581,6 +581,34 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
|
|||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||||
}
|
||||
|
||||
// Write transport data if on transport
|
||||
if (info.hasFlag(MovementFlags::ONTRANSPORT)) {
|
||||
// Write packed transport GUID
|
||||
uint8_t transMask = 0;
|
||||
uint8_t transGuidBytes[8];
|
||||
int transGuidByteCount = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = static_cast<uint8_t>((info.transportGuid >> (i * 8)) & 0xFF);
|
||||
if (byte != 0) {
|
||||
transMask |= (1 << i);
|
||||
transGuidBytes[transGuidByteCount++] = byte;
|
||||
}
|
||||
}
|
||||
packet.writeUInt8(transMask);
|
||||
for (int i = 0; i < transGuidByteCount; i++) {
|
||||
packet.writeUInt8(transGuidBytes[i]);
|
||||
}
|
||||
|
||||
// Write transport local position
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportX), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportY), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportZ), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportO), sizeof(float));
|
||||
|
||||
// Write transport time
|
||||
packet.writeUInt32(info.transportTime);
|
||||
}
|
||||
|
||||
// Detailed hex dump for debugging
|
||||
static int mvLog = 5;
|
||||
if (mvLog-- > 0) {
|
||||
|
|
@ -596,7 +624,11 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
|
|||
" flags=0x", std::hex, info.flags, std::dec,
|
||||
" flags2=0x", std::hex, info.flags2, std::dec,
|
||||
" pos=(", info.x, ",", info.y, ",", info.z, ",", info.orientation, ")",
|
||||
" fallTime=", info.fallTime);
|
||||
" fallTime=", info.fallTime,
|
||||
(info.hasFlag(MovementFlags::ONTRANSPORT) ?
|
||||
" ONTRANSPORT guid=0x" + std::to_string(info.transportGuid) +
|
||||
" localPos=(" + std::to_string(info.transportX) + "," +
|
||||
std::to_string(info.transportY) + "," + std::to_string(info.transportZ) + ")" : ""));
|
||||
LOG_INFO("MOVEPKT hex: ", hex);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/texture.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
|
|
@ -438,6 +439,49 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
" refs: ", modelData.portalRefs.size());
|
||||
}
|
||||
|
||||
// Store doodad templates (M2 models placed in WMO) for instancing later
|
||||
if (!model.doodadSets.empty() && !model.doodads.empty()) {
|
||||
const auto& doodadSet = model.doodadSets[0]; // Use first doodad set
|
||||
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
||||
uint32_t doodadIdx = doodadSet.startIndex + di;
|
||||
if (doodadIdx >= model.doodads.size()) break;
|
||||
|
||||
const auto& doodad = model.doodads[doodadIdx];
|
||||
auto nameIt = model.doodadNames.find(doodad.nameIndex);
|
||||
if (nameIt == model.doodadNames.end()) continue;
|
||||
|
||||
std::string m2Path = nameIt->second;
|
||||
if (m2Path.empty()) continue;
|
||||
|
||||
// Convert .mdx/.mdl to .m2
|
||||
if (m2Path.size() > 4) {
|
||||
std::string ext = m2Path.substr(m2Path.size() - 4);
|
||||
for (char& c : ext) c = std::tolower(c);
|
||||
if (ext == ".mdx" || ext == ".mdl") {
|
||||
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
||||
}
|
||||
}
|
||||
|
||||
// Build doodad's local transform (WoW coordinates)
|
||||
// WMO doodads use quaternion rotation (X/Y swapped for correct orientation)
|
||||
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.y, doodad.rotation.x, doodad.rotation.z);
|
||||
|
||||
glm::mat4 localTransform(1.0f);
|
||||
localTransform = glm::translate(localTransform, doodad.position);
|
||||
localTransform *= glm::mat4_cast(fixedRotation);
|
||||
localTransform = glm::scale(localTransform, glm::vec3(doodad.scale));
|
||||
|
||||
DoodadTemplate doodadTemplate;
|
||||
doodadTemplate.m2Path = m2Path;
|
||||
doodadTemplate.localTransform = localTransform;
|
||||
modelData.doodadTemplates.push_back(doodadTemplate);
|
||||
}
|
||||
|
||||
if (!modelData.doodadTemplates.empty()) {
|
||||
core::Logger::getInstance().info("WMO has ", modelData.doodadTemplates.size(), " doodad templates");
|
||||
}
|
||||
}
|
||||
|
||||
loadedModels[id] = std::move(modelData);
|
||||
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
||||
return true;
|
||||
|
|
@ -594,7 +638,37 @@ void WMORenderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tra
|
|||
inst.worldGroupBounds.emplace_back(gMin, gMax);
|
||||
}
|
||||
}
|
||||
rebuildSpatialIndex();
|
||||
|
||||
// Propagate transform to child M2 doodads (chairs, furniture on transports)
|
||||
if (m2Renderer_ && !inst.doodads.empty()) {
|
||||
for (const auto& doodad : inst.doodads) {
|
||||
glm::mat4 worldTransform = inst.modelMatrix * doodad.localTransform;
|
||||
m2Renderer_->setInstanceTransform(doodad.m2InstanceId, worldTransform);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Don't rebuild spatial index on every transform update - causes flickering
|
||||
// Spatial grid is only used for collision queries, render iterates all instances
|
||||
// rebuildSpatialIndex();
|
||||
}
|
||||
|
||||
void WMORenderer::addDoodadToInstance(uint32_t instanceId, uint32_t m2InstanceId, const glm::mat4& localTransform) {
|
||||
auto it = std::find_if(instances.begin(), instances.end(),
|
||||
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
|
||||
if (it != instances.end()) {
|
||||
WMOInstance::DoodadInfo doodad;
|
||||
doodad.m2InstanceId = m2InstanceId;
|
||||
doodad.localTransform = localTransform;
|
||||
it->doodads.push_back(doodad);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<WMORenderer::DoodadTemplate>* WMORenderer::getDoodadTemplates(uint32_t modelId) const {
|
||||
auto it = loadedModels.find(modelId);
|
||||
if (it != loadedModels.end() && !it->second.doodadTemplates.empty()) {
|
||||
return &it->second.doodadTemplates;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void WMORenderer::removeInstance(uint32_t instanceId) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue