mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +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)
|
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
|
bool looping; // Set to false after adding explicit wrap point
|
||||||
uint32_t durationMs; // Total loop duration in ms (includes wrap segment if added)
|
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 {
|
struct ActiveTransport {
|
||||||
|
|
@ -100,7 +101,7 @@ private:
|
||||||
std::unordered_map<uint64_t, ActiveTransport> transports_;
|
std::unordered_map<uint64_t, ActiveTransport> transports_;
|
||||||
std::unordered_map<uint32_t, TransportPath> paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc)
|
std::unordered_map<uint32_t, TransportPath> paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc)
|
||||||
rendering::WMORenderer* wmoRenderer_ = nullptr;
|
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)
|
float elapsedTime_ = 0.0f; // Total elapsed time (seconds)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,14 @@ struct MovementInfo {
|
||||||
float jumpCosAngle = 0.0f; // Jump horizontal cos
|
float jumpCosAngle = 0.0f; // Jump horizontal cos
|
||||||
float jumpXYSpeed = 0.0f; // Jump horizontal speed
|
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 {
|
bool hasFlag(MovementFlags flag) const {
|
||||||
return (flags & static_cast<uint32_t>(flag)) != 0;
|
return (flags & static_cast<uint32_t>(flag)) != 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace rendering {
|
||||||
class Camera;
|
class Camera;
|
||||||
class Shader;
|
class Shader;
|
||||||
class Frustum;
|
class Frustum;
|
||||||
|
class M2Renderer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WMO (World Model Object) Renderer
|
* WMO (World Model Object) Renderer
|
||||||
|
|
@ -49,6 +50,11 @@ public:
|
||||||
*/
|
*/
|
||||||
void shutdown();
|
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
|
* Load WMO model and create GPU resources
|
||||||
* @param model WMO model with geometry data
|
* @param model WMO model with geometry data
|
||||||
|
|
@ -89,6 +95,27 @@ public:
|
||||||
*/
|
*/
|
||||||
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
|
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
|
* Remove WMO instance
|
||||||
* @param instanceId Instance to remove
|
* @param instanceId Instance to remove
|
||||||
|
|
@ -363,6 +390,10 @@ private:
|
||||||
glm::vec3 boundingBoxMax;
|
glm::vec3 boundingBoxMax;
|
||||||
bool isLowPlatform = false;
|
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)
|
// Texture handles for this model (indexed by texture path order)
|
||||||
std::vector<GLuint> textures;
|
std::vector<GLuint> textures;
|
||||||
|
|
||||||
|
|
@ -510,6 +541,9 @@ private:
|
||||||
// Asset manager for loading textures
|
// Asset manager for loading textures
|
||||||
pipeline::AssetManager* assetManager = nullptr;
|
pipeline::AssetManager* assetManager = nullptr;
|
||||||
|
|
||||||
|
// M2 renderer for hierarchical transforms (doodads following WMO parent)
|
||||||
|
M2Renderer* m2Renderer_ = nullptr;
|
||||||
|
|
||||||
// Current map name for zone-specific floor cache
|
// Current map name for zone-specific floor cache
|
||||||
std::string mapName_;
|
std::string mapName_;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -903,16 +903,10 @@ void Application::setupUICallbacks() {
|
||||||
// Register the transport with spawn position (prevents rendering at origin until server update)
|
// Register the transport with spawn position (prevents rendering at origin until server update)
|
||||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos);
|
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos);
|
||||||
|
|
||||||
if (clientAnim) {
|
// Server-authoritative movement - set initial position from spawn data
|
||||||
LOG_INFO("Transport registered - client-side animation enabled");
|
glm::vec3 canonicalPos(x, y, z);
|
||||||
} else {
|
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
||||||
// Only call updateServerTransport if client animation is disabled
|
LOG_INFO("Transport registered - server-authoritative movement");
|
||||||
// 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
|
// 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");
|
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);
|
showProgress("Loading character model...", 0.05f);
|
||||||
|
|
||||||
// Build faction hostility map for this character's race
|
// 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,
|
LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
|
||||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
" 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
|
// Check if this is a transport and notify via special method
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
std::string lowerModelPath = modelPath;
|
std::string lowerModelPath = modelPath;
|
||||||
|
|
@ -3285,6 +3344,12 @@ void Application::setupTestTransport() {
|
||||||
// Connect transport manager to WMO renderer
|
// Connect transport manager to WMO renderer
|
||||||
transportManager->setWMORenderer(wmoRenderer);
|
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)
|
// Define a simple circular path around Stormwind harbor (canonical coordinates)
|
||||||
// These coordinates are approximate - adjust based on actual harbor layout
|
// These coordinates are approximate - adjust based on actual harbor layout
|
||||||
std::vector<glm::vec3> harborPath = {
|
std::vector<glm::vec3> harborPath = {
|
||||||
|
|
|
||||||
|
|
@ -1775,8 +1775,24 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
break;
|
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,
|
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
|
// Convert canonical → server coordinates for the wire
|
||||||
MovementInfo wireInfo = movementInfo;
|
MovementInfo wireInfo = movementInfo;
|
||||||
|
|
@ -1785,6 +1801,15 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
wireInfo.y = serverPos.y;
|
wireInfo.y = serverPos.y;
|
||||||
wireInfo.z = serverPos.z;
|
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
|
// Build and send movement packet
|
||||||
auto packet = MovementPacket::build(opcode, wireInfo, playerGuid);
|
auto packet = MovementPacket::build(opcode, wireInfo, playerGuid);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
|
|
@ -1869,7 +1894,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (block.guid == playerGuid) {
|
if (block.guid == playerGuid) {
|
||||||
if (block.onTransport) {
|
if (block.onTransport) {
|
||||||
playerTransportGuid_ = block.transportGuid;
|
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,
|
LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec,
|
||||||
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
|
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2359,6 +2386,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
movementInfo.y = pos.y;
|
movementInfo.y = pos.y;
|
||||||
movementInfo.z = pos.z;
|
movementInfo.z = pos.z;
|
||||||
movementInfo.orientation = block.orientation;
|
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
|
// 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);
|
glm::vec3 offset0 = evalTimedCatmullRom(path, 0);
|
||||||
transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn
|
transport.basePosition = spawnWorldPos - offset0; // Infer base from spawn
|
||||||
transport.position = spawnWorldPos; // Start at spawn position (base + offset0)
|
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
|
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.localClockMs = 0;
|
||||||
transport.hasServerClock = false;
|
transport.hasServerClock = false;
|
||||||
transport.serverClockOffsetMs = 0;
|
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.serverYaw = 0.0f;
|
||||||
transport.hasServerYaw = false;
|
transport.hasServerYaw = false;
|
||||||
transport.lastServerUpdate = 0.0f;
|
transport.lastServerUpdate = 0.0f;
|
||||||
|
|
@ -128,33 +139,53 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
|
||||||
|
|
||||||
TransportPath path;
|
TransportPath path;
|
||||||
path.pathId = pathId;
|
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)
|
// Single point = stationary (durationMs = 0)
|
||||||
// Multiple points = assume constant speed
|
|
||||||
path.points.reserve(waypoints.size());
|
|
||||||
if (waypoints.size() == 1) {
|
if (waypoints.size() == 1) {
|
||||||
path.points.push_back({0, waypoints[0]});
|
path.points.push_back({0, waypoints[0]});
|
||||||
path.durationMs = 0; // Stationary
|
path.durationMs = 0;
|
||||||
} else {
|
path.looping = false;
|
||||||
// Calculate cumulative time based on distance and speed
|
paths_[pathId] = path;
|
||||||
uint32_t cumulativeMs = 0;
|
LOG_INFO("TransportManager: Loaded stationary path ", pathId);
|
||||||
path.points.push_back({0, waypoints[0]});
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
paths_[pathId] = path;
|
||||||
|
|
||||||
LOG_INFO("TransportManager: Loaded path ", pathId,
|
LOG_INFO("TransportManager: Loaded path ", pathId,
|
||||||
" with ", waypoints.size(), " waypoints, "
|
" with ", waypoints.size(), " waypoints",
|
||||||
"looping=", looping, ", speed=", speed);
|
(looping ? " + wrap segment" : ""),
|
||||||
|
", duration=", path.durationMs, "ms, speed=", speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransportManager::setDeckBounds(uint64_t guid, const glm::vec3& min, const glm::vec3& max) {
|
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) {
|
if (!found) {
|
||||||
segmentIdx = path.looping ? (path.points.size() - 1) :
|
// For time-closed paths (explicit wrap point), last valid segment is points.size() - 2
|
||||||
(path.points.size() >= 2 ? path.points.size() - 2 : 0);
|
segmentIdx = (path.points.size() >= 2) ? (path.points.size() - 2) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t numPoints = path.points.size();
|
size_t numPoints = path.points.size();
|
||||||
|
|
||||||
// Get 4 control points for Catmull-Rom
|
// Get 4 control points for Catmull-Rom
|
||||||
size_t p0Idx = (segmentIdx == 0) ? (path.looping ? numPoints - 1 : 0) : segmentIdx - 1;
|
// Helper to clamp index (no wrapping for non-looping paths)
|
||||||
size_t p1Idx = segmentIdx;
|
auto idxClamp = [&](size_t i) -> size_t {
|
||||||
size_t p2Idx = (segmentIdx + 1) % numPoints;
|
return (i >= numPoints) ? (numPoints - 1) : i;
|
||||||
size_t p3Idx = (segmentIdx + 2) % numPoints;
|
};
|
||||||
|
|
||||||
if (!path.looping) {
|
size_t p0Idx, p1Idx, p2Idx, p3Idx;
|
||||||
if (segmentIdx == 0) p0Idx = 0;
|
p1Idx = segmentIdx;
|
||||||
if (segmentIdx >= numPoints - 2) p3Idx = numPoints - 1;
|
|
||||||
if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1;
|
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;
|
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) {
|
if (!found) {
|
||||||
segmentIdx = path.looping ? (path.points.size() - 1) :
|
// For time-closed paths (explicit wrap point), last valid segment is points.size() - 2
|
||||||
(path.points.size() >= 2 ? path.points.size() - 2 : 0);
|
segmentIdx = (path.points.size() >= 2) ? (path.points.size() - 2) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t numPoints = path.points.size();
|
size_t numPoints = path.points.size();
|
||||||
|
|
||||||
// Get 4 control points
|
// Get 4 control points for tangent calculation
|
||||||
size_t p0Idx = (segmentIdx == 0) ? (path.looping ? numPoints - 1 : 0) : segmentIdx - 1;
|
// Helper to clamp index (no wrapping for non-looping paths)
|
||||||
size_t p1Idx = segmentIdx;
|
auto idxClamp = [&](size_t i) -> size_t {
|
||||||
size_t p2Idx = (segmentIdx + 1) % numPoints;
|
return (i >= numPoints) ? (numPoints - 1) : i;
|
||||||
size_t p3Idx = (segmentIdx + 2) % numPoints;
|
};
|
||||||
|
|
||||||
if (!path.looping) {
|
size_t p0Idx, p1Idx, p2Idx, p3Idx;
|
||||||
if (segmentIdx == 0) p0Idx = 0;
|
p1Idx = segmentIdx;
|
||||||
if (segmentIdx >= numPoints - 2) p3Idx = numPoints - 1;
|
|
||||||
if (segmentIdx >= numPoints - 1) p2Idx = numPoints - 1;
|
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;
|
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;
|
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
|
// Seed basePosition from t=0 assumption before first search
|
||||||
// (t=0 corresponds to spawn point / first path point)
|
// (t=0 corresponds to spawn point / first path point)
|
||||||
if (!transport->hasServerClock) {
|
if (!transport->hasServerClock) {
|
||||||
|
|
@ -659,6 +728,14 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
||||||
" u32=(", ux, ",", uy, ",", uz, ")");
|
" 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)});
|
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
|
// TransportAnimation.dbc uses server coordinates - convert to canonical
|
||||||
glm::vec3 canonical = core::coords::serverToCanonical(pos);
|
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)
|
// Debug waypoint conversion for first transport (entry 2074)
|
||||||
if (transportEntry == 2074 && idx < 5) {
|
if (transportEntry == 2074 && idx < 5) {
|
||||||
LOG_INFO("COORD CONVERT: entry=", transportEntry, " t=", tMs,
|
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, ")");
|
" → 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
|
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;
|
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
|
// Store path
|
||||||
TransportPath path;
|
TransportPath path;
|
||||||
path.pathId = transportEntry;
|
path.pathId = transportEntry;
|
||||||
path.points = timedPoints;
|
path.points = timedPoints;
|
||||||
// Keep looping=true even with duplicate wrap point for smooth control point selection at seam
|
// CRITICAL: We added an explicit wrap point (last → first), so this is TIME-CLOSED, not index-wrapped
|
||||||
// This prevents kinks on the last segment approaching the wrap
|
// Setting looping=false ensures evalTimedCatmullRom uses clamp logic (not modulo) for control points
|
||||||
path.looping = true;
|
path.looping = false;
|
||||||
path.durationMs = durationMs;
|
path.durationMs = durationMs;
|
||||||
|
path.zOnly = isZOnly;
|
||||||
paths_[transportEntry] = path;
|
paths_[transportEntry] = path;
|
||||||
pathsLoaded++;
|
pathsLoaded++;
|
||||||
|
|
||||||
|
|
@ -738,6 +846,7 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
||||||
glm::vec3 lastOffset = timedPoints[timedPoints.size() - 2].pos; // -2 to skip wrap duplicate
|
glm::vec3 lastOffset = timedPoints[timedPoints.size() - 2].pos; // -2 to skip wrap duplicate
|
||||||
LOG_INFO(" Transport ", transportEntry, ": ", timedPoints.size() - 1, " waypoints + wrap, ",
|
LOG_INFO(" Transport ", transportEntry, ": ", timedPoints.size() - 1, " waypoints + wrap, ",
|
||||||
durationMs, "ms duration (wrap=", wrapMs, "ms, t0_normalized=", timedPoints[0].tMs, "ms)",
|
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, ")",
|
" firstOffset=(", firstOffset.x, ", ", firstOffset.y, ", ", firstOffset.z, ")",
|
||||||
" midOffset=(", midOffset.x, ", ", midOffset.y, ", ", midOffset.z, ")",
|
" midOffset=(", midOffset.x, ", ", midOffset.y, ", ", midOffset.z, ")",
|
||||||
" lastOffset=(", lastOffset.x, ", ", lastOffset.y, ", ", lastOffset.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));
|
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
|
// Detailed hex dump for debugging
|
||||||
static int mvLog = 5;
|
static int mvLog = 5;
|
||||||
if (mvLog-- > 0) {
|
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,
|
" flags=0x", std::hex, info.flags, std::dec,
|
||||||
" flags2=0x", std::hex, info.flags2, std::dec,
|
" flags2=0x", std::hex, info.flags2, std::dec,
|
||||||
" pos=(", info.x, ",", info.y, ",", info.z, ",", info.orientation, ")",
|
" 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);
|
LOG_INFO("MOVEPKT hex: ", hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "rendering/wmo_renderer.hpp"
|
#include "rendering/wmo_renderer.hpp"
|
||||||
|
#include "rendering/m2_renderer.hpp"
|
||||||
#include "rendering/texture.hpp"
|
#include "rendering/texture.hpp"
|
||||||
#include "rendering/shader.hpp"
|
#include "rendering/shader.hpp"
|
||||||
#include "rendering/camera.hpp"
|
#include "rendering/camera.hpp"
|
||||||
|
|
@ -438,6 +439,49 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
" refs: ", modelData.portalRefs.size());
|
" 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);
|
loadedModels[id] = std::move(modelData);
|
||||||
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -594,7 +638,37 @@ void WMORenderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tra
|
||||||
inst.worldGroupBounds.emplace_back(gMin, gMax);
|
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) {
|
void WMORenderer::removeInstance(uint32_t instanceId) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue