Implement transport spline movement and fix SMSG_QUESTLOG_FULL

SMSG_MONSTER_MOVE_TRANSPORT (handleMonsterMoveTransport):
- Parse full WotLK 3.3.5a spline payload after the transport-local start
  position: splineId, moveType, facing data (spot/target/angle), splineFlags,
  Animation flag block, duration, Parabolic flag block, pointCount, waypoints
- Extract destination in transport-local server coords, compose to world
  space via TransportManager, then call entity->startMoveTo() with the
  spline duration so NPC movement interpolates smoothly instead of teleporting
- Handle all facing modes (FacingSpot/Target/Angle/normal) in transport space
- Degenerate cases (no spline data, moveType==1 stop, no transport manager)
  fall back to snapping start position as before

SMSG_QUESTLOG_FULL:
- This opcode is a zero-payload notification meaning the quest log is at
  capacity (25 quests); it does not carry quest log data
- Replace placeholder LOG_INFO stubs with a proper "Your quest log is full."
  chat notification and a single LOG_INFO
This commit is contained in:
Kelsi 2026-03-09 13:04:35 -07:00
parent b0d7dbc32c
commit 6a7287bde3

View file

@ -2703,13 +2703,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
pendingQuestQueryIds_.erase(questId);
break;
}
case Opcode::SMSG_QUESTLOG_FULL: {
LOG_INFO("***** RECEIVED SMSG_QUESTLOG_FULL *****");
LOG_INFO(" Packet size: ", packet.getSize());
LOG_INFO(" Server uses SMSG_QUESTLOG_FULL for quest log sync!");
// TODO: Parse quest log entries from this packet
case Opcode::SMSG_QUESTLOG_FULL:
// Zero-payload notification: the player's quest log is full (25 quests).
addSystemChatMessage("Your quest log is full.");
LOG_INFO("SMSG_QUESTLOG_FULL: quest log is at capacity");
break;
}
case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS:
handleQuestRequestItems(packet);
break;
@ -9300,50 +9298,154 @@ 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)
// Packet: moverGuid(8) + unk(1) + transportGuid(8) + localX/Y/Z(12) + spline data
if (packet.getSize() - packet.getReadPos() < 8 + 1 + 8 + 12) return;
uint64_t moverGuid = packet.readUInt64();
uint8_t unk = packet.readUInt8(); // Unknown byte (usually 0)
/*uint8_t unk =*/ packet.readUInt8();
uint64_t transportGuid = packet.readUInt64();
// Transport-local coordinates (server space)
// Transport-local start position (server coords: x=east/west, y=north/south, z=up)
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");
if (!entity) return;
// ---- Spline data (same format as SMSG_MONSTER_MOVE, transport-local coords) ----
if (packet.getReadPos() + 5 > packet.getSize()) {
// No spline data — snap to start position
if (transportManager_) {
glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, false, 0.0f);
glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localCanonical);
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
if (entity->getType() == ObjectType::UNIT && creatureMoveCallback_)
creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
}
return;
}
if (transportManager_) {
// Use TransportManager to compose world position from local offset
glm::vec3 localPosCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
setTransportAttachment(moverGuid, entity->getType(), transportGuid, localPosCanonical, false, 0.0f);
glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localPosCanonical);
/*uint32_t splineId =*/ packet.readUInt32();
uint8_t moveType = packet.readUInt8();
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
LOG_INFO(" Composed NPC world position: (", worldPos.x, ", ", worldPos.y, ", ", worldPos.z, ")");
if (entity->getType() == ObjectType::UNIT && creatureMoveCallback_) {
creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
if (moveType == 1) {
// Stop — snap to start position
if (transportManager_) {
glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, false, 0.0f);
glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localCanonical);
entity->setPosition(worldPos.x, worldPos.y, worldPos.z, entity->getOrientation());
if (entity->getType() == ObjectType::UNIT && creatureMoveCallback_)
creatureMoveCallback_(moverGuid, worldPos.x, worldPos.y, worldPos.z, 0);
}
} else {
LOG_WARNING(" TransportManager not available for NPC position composition");
return;
}
// TODO: Parse full spline data for smooth NPC movement on transport
// Then update entity position and call creatureMoveCallback_
// Facing data based on moveType
float facingAngle = entity->getOrientation();
if (moveType == 2) { // FacingSpot
if (packet.getReadPos() + 12 > packet.getSize()) return;
float sx = packet.readFloat(), sy = packet.readFloat(), sz = packet.readFloat();
facingAngle = std::atan2(-(sy - localY), sx - localX);
(void)sz;
} else if (moveType == 3) { // FacingTarget
if (packet.getReadPos() + 8 > packet.getSize()) return;
uint64_t tgtGuid = packet.readUInt64();
if (auto tgt = entityManager.getEntity(tgtGuid)) {
float dx = tgt->getX() - entity->getX();
float dy = tgt->getY() - entity->getY();
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f)
facingAngle = std::atan2(-dy, dx);
}
} else if (moveType == 4) { // FacingAngle
if (packet.getReadPos() + 4 > packet.getSize()) return;
facingAngle = core::coords::serverToCanonicalYaw(packet.readFloat());
}
// Suppress unused variable warning for now
(void)unk;
if (packet.getReadPos() + 4 > packet.getSize()) return;
uint32_t splineFlags = packet.readUInt32();
if (splineFlags & 0x00400000) { // Animation
if (packet.getReadPos() + 5 > packet.getSize()) return;
packet.readUInt8(); packet.readUInt32();
}
if (packet.getReadPos() + 4 > packet.getSize()) return;
uint32_t duration = packet.readUInt32();
if (splineFlags & 0x00000800) { // Parabolic
if (packet.getReadPos() + 8 > packet.getSize()) return;
packet.readFloat(); packet.readUInt32();
}
if (packet.getReadPos() + 4 > packet.getSize()) return;
uint32_t pointCount = packet.readUInt32();
// Read destination point (transport-local server coords)
float destLocalX = localX, destLocalY = localY, destLocalZ = localZ;
bool hasDest = false;
if (pointCount > 0) {
const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0;
if (uncompressed) {
for (uint32_t i = 0; i < pointCount - 1; ++i) {
if (packet.getReadPos() + 12 > packet.getSize()) break;
packet.readFloat(); packet.readFloat(); packet.readFloat();
}
if (packet.getReadPos() + 12 <= packet.getSize()) {
destLocalX = packet.readFloat();
destLocalY = packet.readFloat();
destLocalZ = packet.readFloat();
hasDest = true;
}
} else {
if (packet.getReadPos() + 12 <= packet.getSize()) {
destLocalX = packet.readFloat();
destLocalY = packet.readFloat();
destLocalZ = packet.readFloat();
hasDest = true;
}
}
}
if (!transportManager_) {
LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: TransportManager not available for mover 0x",
std::hex, moverGuid, std::dec);
return;
}
glm::vec3 startLocalCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ));
if (hasDest && duration > 0) {
glm::vec3 destLocalCanonical = core::coords::serverToCanonical(glm::vec3(destLocalX, destLocalY, destLocalZ));
glm::vec3 startWorld = transportManager_->getPlayerWorldPosition(transportGuid, startLocalCanonical);
glm::vec3 destWorld = transportManager_->getPlayerWorldPosition(transportGuid, destLocalCanonical);
// Face toward destination unless an explicit facing was given
if (moveType == 0) {
float dx = destLocalCanonical.x - startLocalCanonical.x;
float dy = destLocalCanonical.y - startLocalCanonical.y;
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f)
facingAngle = std::atan2(-dy, dx);
}
setTransportAttachment(moverGuid, entity->getType(), transportGuid, destLocalCanonical, false, 0.0f);
entity->startMoveTo(destWorld.x, destWorld.y, destWorld.z, facingAngle, duration / 1000.0f);
if (entity->getType() == ObjectType::UNIT && creatureMoveCallback_)
creatureMoveCallback_(moverGuid, destWorld.x, destWorld.y, destWorld.z, duration);
LOG_DEBUG("SMSG_MONSTER_MOVE_TRANSPORT: mover=0x", std::hex, moverGuid,
" transport=0x", transportGuid, std::dec,
" dur=", duration, "ms dest=(", destWorld.x, ",", destWorld.y, ",", destWorld.z, ")");
} else {
glm::vec3 startWorld = transportManager_->getPlayerWorldPosition(transportGuid, startLocalCanonical);
setTransportAttachment(moverGuid, entity->getType(), transportGuid, startLocalCanonical, false, 0.0f);
entity->setPosition(startWorld.x, startWorld.y, startWorld.z, facingAngle);
if (entity->getType() == ObjectType::UNIT && creatureMoveCallback_)
creatureMoveCallback_(moverGuid, startWorld.x, startWorld.y, startWorld.z, 0);
}
}
void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {