Merge master into chore/god-object-decomposition-2nd

Resolve conflicts:
- audio_callback_handler.cpp: keep PR's animation_controller include
- movement_handler.cpp: use PR accessors with master's transportResolved logic
- world_packets.cpp: keep PR's decomposed version (functions moved to split files)

Apply overkill field fix to world_packets_entity.cpp (WotLK
SMSG_ATTACKERSTATEUPDATE missing uint32 overkill between damage and
subDamageCount).
This commit is contained in:
Kelsi 2026-04-05 19:42:25 -07:00
commit e32f4fbff9
9 changed files with 148 additions and 35 deletions

View file

@ -191,7 +191,7 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT, size=", packet.getSize());
return;
}
LOG_INFO("INCOMING CHAT: type=", static_cast<int>(data.type),
LOG_WARNING("INCOMING CHAT: type=", static_cast<int>(data.type),
" (", getChatTypeString(data.type), ") sender=0x", std::hex, data.senderGuid, std::dec,
" '", data.senderName, "' msg='", data.message.substr(0, 60), "'");

View file

@ -1342,7 +1342,11 @@ void GameHandler::mailMarkAsRead(uint32_t mailId) {
glm::vec3 GameHandler::getComposedWorldPosition() {
if (playerTransportGuid_ != 0 && transportManager_) {
return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
auto* tr = transportManager_->getTransport(playerTransportGuid_);
if (tr) {
return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
}
// Transport not tracked — fall through to normal position
}
// Not on transport, return normal movement position
return glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z);

View file

@ -515,12 +515,28 @@ void MovementHandler::sendMovement(Opcode opcode) {
// Add transport data if player is on a server-recognized transport
if (includeTransportInWire) {
bool transportResolved = false;
if (owner_.getTransportManager()) {
glm::vec3 composed = owner_.getTransportManager()->getPlayerWorldPosition(owner_.playerTransportGuidRef(), owner_.playerTransportOffsetRef());
movementInfo.x = composed.x;
movementInfo.y = composed.y;
movementInfo.z = composed.z;
auto* tr = owner_.getTransportManager()->getTransport(owner_.playerTransportGuidRef());
if (tr) {
transportResolved = true;
glm::vec3 composed = owner_.getTransportManager()->getPlayerWorldPosition(owner_.playerTransportGuidRef(), owner_.playerTransportOffsetRef());
movementInfo.x = composed.x;
movementInfo.y = composed.y;
movementInfo.z = composed.z;
}
}
if (!transportResolved) {
// Transport not tracked — don't send ONTRANSPORT to the server.
// Sending stale transport GUID + local offset causes the server to
// compute a bad world position and teleport us to map origin.
LOG_WARNING("sendMovement: transport 0x", std::hex, owner_.playerTransportGuidRef(),
std::dec, " not found — clearing transport state");
includeTransportInWire = false;
owner_.clearPlayerTransport();
}
}
if (includeTransportInWire) {
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
movementInfo.transportGuid = owner_.playerTransportGuidRef();
movementInfo.transportX = owner_.playerTransportOffsetRef().x;
@ -596,6 +612,17 @@ void MovementHandler::sendMovement(Opcode opcode) {
wireOpcode(opcode), std::dec,
(includeTransportInWire ? " ONTRANSPORT" : ""));
// Detect near-origin position on Eastern Kingdoms (map 0) — this would place
// the player near Alterac Mountains and is almost certainly a bug.
if (owner_.getCurrentMapId() == 0 &&
std::abs(movementInfo.x) < 500.0f && std::abs(movementInfo.y) < 500.0f) {
LOG_WARNING("sendMovement: position near map origin! canonical=(",
movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z,
") onTransport=", owner_.isOnTransport(),
" transportGuid=0x", std::hex, owner_.playerTransportGuidRef(), std::dec,
" flags=0x", std::hex, movementInfo.flags, std::dec);
}
// Convert canonical → server coordinates for the wire
MovementInfo wireInfo = movementInfo;
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wireInfo.x, wireInfo.y, wireInfo.z));
@ -603,10 +630,12 @@ void MovementHandler::sendMovement(Opcode opcode) {
wireInfo.y = serverPos.y;
wireInfo.z = serverPos.z;
// Periodic position audit — DEBUG to avoid flooding production logs.
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 30 == 0) {
LOG_DEBUG("HEARTBEAT pos canonical=(", movementInfo.x, ",", movementInfo.y, ",", movementInfo.z,
") wire=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z, ")");
// Periodic position audit — log every ~60 heartbeats (~30s) to trace position drift.
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 60 == 0) {
LOG_WARNING("HEARTBEAT #", heartbeatLogCount_, " canonical=(",
movementInfo.x, ",", movementInfo.y, ",", movementInfo.z,
") server=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z,
") flags=0x", std::hex, movementInfo.flags, std::dec);
}
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
@ -1644,9 +1673,10 @@ void MovementHandler::handleTeleportAck(network::Packet& packet) {
float serverZ = packet.readFloat();
float orientation = packet.readFloat();
LOG_INFO("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
" counter=", counter,
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")");
LOG_WARNING("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
" counter=", counter,
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
" currentPos=(", movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z, ")");
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
movementInfo.x = canonical.x;
@ -2535,6 +2565,17 @@ void MovementHandler::checkAreaTriggers() {
const float py = movementInfo.y;
const float pz = movementInfo.z;
// Sanity: if position is near map origin on Eastern Kingdoms (map 0),
// something has corrupted movementInfo — skip area trigger check to
// avoid firing Alterac/Hillsbrad triggers and causing a rogue teleport.
if (owner_.getCurrentMapId() == 0 && std::abs(px) < 500.0f && std::abs(py) < 500.0f) {
LOG_WARNING("checkAreaTriggers: position near map origin (", px, ", ", py, ", ", pz,
") on map 0 — skipping to avoid rogue teleport. onTransport=",
owner_.isOnTransport(), " transportGuid=0x", std::hex,
owner_.playerTransportGuidRef(), std::dec);
return;
}
// On first check after map transfer, just mark which triggers we're inside
// without firing them — prevents exit portal from immediately sending us back
bool suppressFirst = owner_.areaTriggerSuppressFirstRef();

View file

@ -141,7 +141,9 @@ ActiveTransport* TransportManager::getTransport(uint64_t guid) {
glm::vec3 TransportManager::getPlayerWorldPosition(uint64_t transportGuid, const glm::vec3& localOffset) {
auto* transport = getTransport(transportGuid);
if (!transport) {
return localOffset; // Fallback
LOG_WARNING("getPlayerWorldPosition: transport 0x", std::hex, transportGuid, std::dec,
" not found — returning localOffset as-is (callers should guard)");
return localOffset;
}
if (transport->isM2) {

View file

@ -794,8 +794,8 @@ bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) {
}
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
// Upfront validation: hitInfo(4) + packed GUIDs(1-8 each) + totalDamage(4) + subDamageCount(1) = 13 bytes minimum
if (!packet.hasRemaining(13)) return false;
// Upfront validation: hitInfo(4) + packed GUIDs(1-8 each) + totalDamage(4) + overkill(4) + subDamageCount(1) = 17 bytes minimum
if (!packet.hasRemaining(17)) return false;
size_t startPos = packet.getReadPos();
data.hitInfo = packet.readUInt32();
@ -810,13 +810,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
}
data.targetGuid = packet.readPackedGuid();
// Validate totalDamage + subDamageCount can be read (5 bytes)
if (!packet.hasRemaining(5)) {
// Validate totalDamage + overkill + subDamageCount can be read (9 bytes)
// WotLK (AzerothCore) sends: damage(4) + overkill(4) + subDamageCount(1)
if (!packet.hasRemaining(9)) {
packet.setReadPos(startPos);
return false;
}
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
data.overkill = static_cast<int32_t>(packet.readUInt32());
data.subDamageCount = packet.readUInt8();
// Cap subDamageCount: each entry is 20 bytes. If the claimed count
@ -853,17 +855,14 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
// Validate victimState + overkill fields (8 bytes)
if (!packet.hasRemaining(8)) {
data.victimState = 0;
data.overkill = 0;
return !data.subDamages.empty();
}
data.victimState = packet.readUInt32();
// WotLK (AzerothCore): two unknown uint32 fields follow victimState before overkill.
// Older parsers omitted these, reading overkill from the wrong offset.
// WotLK: attackerState(4) + meleeSpellId(4) follow victimState
auto rem = [&]() { return packet.getRemainingSize(); };
if (rem() >= 4) packet.readUInt32(); // unk1 (always 0)
if (rem() >= 4) packet.readUInt32(); // unk2 (melee spell ID, 0 for auto-attack)
data.overkill = (rem() >= 4) ? static_cast<int32_t>(packet.readUInt32()) : -1;
if (rem() >= 4) packet.readUInt32(); // attackerState (always 0)
if (rem() >= 4) packet.readUInt32(); // meleeSpellId (0 for auto-attack)
// hitInfo-conditional fields: HITINFO_BLOCK(0x2000), RAGE_GAIN(0x20000), FAKE_DAMAGE(0x40)
if ((data.hitInfo & 0x2000) && rem() >= 4) data.blocked = packet.readUInt32();