mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Fix Turtle monster movement packet parsing
This commit is contained in:
parent
9a950ce09f
commit
86127f0ddf
2 changed files with 269 additions and 48 deletions
|
|
@ -612,8 +612,8 @@ void GameHandler::update(float deltaTime) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distance cull other entities
|
// Distance cull other entities (use latest position to avoid culling by stale origin)
|
||||||
glm::vec3 entityPos(entity->getX(), entity->getY(), entity->getZ());
|
glm::vec3 entityPos(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||||
float distSq = glm::dot(entityPos - playerPos, entityPos - playerPos);
|
float distSq = glm::dot(entityPos - playerPos, entityPos - playerPos);
|
||||||
if (distSq < updateRadiusSq) {
|
if (distSq < updateRadiusSq) {
|
||||||
entity->updateMovement(deltaTime);
|
entity->updateMovement(deltaTime);
|
||||||
|
|
@ -664,7 +664,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
// Translate wire opcode to logical opcode via expansion table
|
// Translate wire opcode to logical opcode via expansion table
|
||||||
auto logicalOp = opcodeTable_.fromWire(opcode);
|
auto logicalOp = opcodeTable_.fromWire(opcode);
|
||||||
if (!logicalOp) {
|
if (!logicalOp) {
|
||||||
LOG_DEBUG("Unknown wire opcode 0x", std::hex, opcode, std::dec, " - ignoring");
|
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -861,6 +861,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleMonsterMove(packet);
|
handleMonsterMove(packet);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Opcode::SMSG_COMPRESSED_MOVES:
|
||||||
|
handleCompressedMoves(packet);
|
||||||
|
break;
|
||||||
|
|
||||||
case Opcode::SMSG_MONSTER_MOVE_TRANSPORT:
|
case Opcode::SMSG_MONSTER_MOVE_TRANSPORT:
|
||||||
handleMonsterMoveTransport(packet);
|
handleMonsterMoveTransport(packet);
|
||||||
break;
|
break;
|
||||||
|
|
@ -4063,6 +4067,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(),
|
gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(),
|
||||||
entity->getZ(), entity->getOrientation());
|
entity->getZ(), entity->getOrientation());
|
||||||
}
|
}
|
||||||
|
// Fire move callback for non-player units (creatures).
|
||||||
|
// SMSG_MONSTER_MOVE handles smooth interpolated movement, but many
|
||||||
|
// servers (especially vanilla/Turtle WoW) communicate NPC positions
|
||||||
|
// via MOVEMENT blocks instead. Use duration=0 for an instant snap.
|
||||||
|
if (block.guid != playerGuid &&
|
||||||
|
entity->getType() == ObjectType::UNIT &&
|
||||||
|
transportGuids_.count(block.guid) == 0 &&
|
||||||
|
creatureMoveCallback_) {
|
||||||
|
creatureMoveCallback_(block.guid, pos.x, pos.y, pos.z, 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
||||||
}
|
}
|
||||||
|
|
@ -6821,66 +6835,178 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::handleCompressedMoves(network::Packet& packet) {
|
||||||
|
// Vanilla/Classic SMSG_COMPRESSED_MOVES: raw concatenated sub-packets, NOT zlib.
|
||||||
|
// Evidence: observed 1-byte "00" packets which are not valid zlib streams.
|
||||||
|
// Each sub-packet: uint8 size (of opcode[2]+payload), uint16 opcode, uint8[] payload.
|
||||||
|
// size=0 → invalid/empty, signals end of batch.
|
||||||
|
const auto& data = packet.getData();
|
||||||
|
size_t dataLen = data.size();
|
||||||
|
|
||||||
|
// Wire opcodes for sub-packet routing
|
||||||
|
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
|
||||||
|
uint16_t monsterMoveTransportWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE_TRANSPORT);
|
||||||
|
|
||||||
|
// Track unhandled sub-opcodes once per compressed packet (avoid log spam)
|
||||||
|
std::unordered_set<uint16_t> unhandledSeen;
|
||||||
|
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < dataLen) {
|
||||||
|
if (pos + 1 > dataLen) break;
|
||||||
|
uint8_t subSize = data[pos];
|
||||||
|
if (subSize < 2) break; // size=0 or 1 → empty/end-of-batch sentinel
|
||||||
|
if (pos + 1 + subSize > dataLen) {
|
||||||
|
LOG_WARNING("SMSG_COMPRESSED_MOVES: sub-packet overruns buffer at pos=", pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint16_t subOpcode = static_cast<uint16_t>(data[pos + 1]) |
|
||||||
|
(static_cast<uint16_t>(data[pos + 2]) << 8);
|
||||||
|
size_t payloadLen = subSize - 2;
|
||||||
|
size_t payloadStart = pos + 3;
|
||||||
|
|
||||||
|
std::vector<uint8_t> subPayload(data.begin() + payloadStart,
|
||||||
|
data.begin() + payloadStart + payloadLen);
|
||||||
|
network::Packet subPacket(subOpcode, subPayload);
|
||||||
|
|
||||||
|
if (subOpcode == monsterMoveWire) {
|
||||||
|
handleMonsterMove(subPacket);
|
||||||
|
} else if (subOpcode == monsterMoveTransportWire) {
|
||||||
|
handleMonsterMoveTransport(subPacket);
|
||||||
|
} else {
|
||||||
|
if (unhandledSeen.insert(subOpcode).second) {
|
||||||
|
LOG_INFO("SMSG_COMPRESSED_MOVES: unhandled sub-opcode 0x",
|
||||||
|
std::hex, subOpcode, std::dec, " payloadLen=", payloadLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = payloadStart + payloadLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
MonsterMoveData data;
|
MonsterMoveData data;
|
||||||
if (!MonsterMoveParser::parse(packet, data)) {
|
// Turtle WoW (1.17+) compresses each SMSG_MONSTER_MOVE individually:
|
||||||
|
// format: uint32 decompressedSize + zlib data (zlib magic = 0x78 ??)
|
||||||
|
const auto& rawData = packet.getData();
|
||||||
|
bool isCompressed = rawData.size() >= 6 &&
|
||||||
|
rawData[4] == 0x78 &&
|
||||||
|
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
|
||||||
|
rawData[5] == 0xDA || rawData[5] == 0x5E);
|
||||||
|
if (isCompressed) {
|
||||||
|
uint32_t decompSize = static_cast<uint32_t>(rawData[0]) |
|
||||||
|
(static_cast<uint32_t>(rawData[1]) << 8) |
|
||||||
|
(static_cast<uint32_t>(rawData[2]) << 16) |
|
||||||
|
(static_cast<uint32_t>(rawData[3]) << 24);
|
||||||
|
if (decompSize == 0 || decompSize > 65536) {
|
||||||
|
LOG_WARNING("SMSG_MONSTER_MOVE: bad decompSize=", decompSize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> decompressed(decompSize);
|
||||||
|
uLongf destLen = decompSize;
|
||||||
|
int ret = uncompress(decompressed.data(), &destLen,
|
||||||
|
rawData.data() + 4, rawData.size() - 4);
|
||||||
|
if (ret != Z_OK) {
|
||||||
|
LOG_WARNING("SMSG_MONSTER_MOVE: zlib error ", ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
decompressed.resize(destLen);
|
||||||
|
// Dump ALL bytes for format diagnosis (remove once confirmed)
|
||||||
|
static int dumpCount = 0;
|
||||||
|
if (dumpCount < 10) {
|
||||||
|
++dumpCount;
|
||||||
|
std::string hex;
|
||||||
|
for (size_t i = 0; i < destLen; ++i) {
|
||||||
|
char buf[4]; snprintf(buf, sizeof(buf), "%02X ", decompressed[i]); hex += buf;
|
||||||
|
}
|
||||||
|
LOG_INFO("MonsterMove decomp[", destLen, "]: ", hex);
|
||||||
|
}
|
||||||
|
// Some Turtle WoW compressed move payloads include an inner
|
||||||
|
// sub-packet wrapper: uint8 size + uint16 opcode + payload.
|
||||||
|
// Do not key this on expansion opcode mappings; strip by structure.
|
||||||
|
std::vector<uint8_t> parseBytes = decompressed;
|
||||||
|
if (destLen >= 3) {
|
||||||
|
uint8_t subSize = decompressed[0];
|
||||||
|
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + subSize bytes
|
||||||
|
uint16_t innerOpcode = static_cast<uint16_t>(decompressed[1]) |
|
||||||
|
(static_cast<uint16_t>(decompressed[2]) << 8);
|
||||||
|
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
|
||||||
|
bool looksLikeMonsterMoveWrapper =
|
||||||
|
(innerOpcode == 0x00DD) || (innerOpcode == monsterMoveWire);
|
||||||
|
// Strict case: one exact wrapped sub-packet in this decompressed blob.
|
||||||
|
if (subSize >= 2 && wrappedLen == destLen && looksLikeMonsterMoveWrapper) {
|
||||||
|
size_t payloadStart = 3;
|
||||||
|
size_t payloadLen = static_cast<size_t>(subSize) - 2;
|
||||||
|
parseBytes.assign(decompressed.begin() + payloadStart,
|
||||||
|
decompressed.begin() + payloadStart + payloadLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network::Packet decompPacket(packet.getOpcode(), parseBytes);
|
||||||
|
if (!MonsterMoveParser::parseVanilla(decompPacket, data)) {
|
||||||
|
LOG_WARNING("Failed to parse vanilla SMSG_MONSTER_MOVE (decompressed ",
|
||||||
|
destLen, " bytes, parseBytes ", parseBytes.size(), " bytes)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!MonsterMoveParser::parse(packet, data)) {
|
||||||
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
|
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update entity position in entity manager
|
// Update entity position in entity manager
|
||||||
auto entity = entityManager.getEntity(data.guid);
|
auto entity = entityManager.getEntity(data.guid);
|
||||||
if (entity) {
|
if (!entity) {
|
||||||
if (data.hasDest) {
|
return;
|
||||||
// Convert destination from server to canonical coords
|
}
|
||||||
glm::vec3 destCanonical = core::coords::serverToCanonical(
|
|
||||||
glm::vec3(data.destX, data.destY, data.destZ));
|
|
||||||
|
|
||||||
// Calculate facing angle
|
if (data.hasDest) {
|
||||||
float orientation = entity->getOrientation();
|
// Convert destination from server to canonical coords
|
||||||
if (data.moveType == 4) {
|
glm::vec3 destCanonical = core::coords::serverToCanonical(
|
||||||
// FacingAngle - server specifies exact angle
|
glm::vec3(data.destX, data.destY, data.destZ));
|
||||||
orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
|
|
||||||
} else if (data.moveType == 3) {
|
// Calculate facing angle
|
||||||
// FacingTarget - face toward the target entity
|
float orientation = entity->getOrientation();
|
||||||
auto target = entityManager.getEntity(data.facingTarget);
|
if (data.moveType == 4) {
|
||||||
if (target) {
|
// FacingAngle - server specifies exact angle
|
||||||
float dx = target->getX() - entity->getX();
|
orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
|
||||||
float dy = target->getY() - entity->getY();
|
} else if (data.moveType == 3) {
|
||||||
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
// FacingTarget - face toward the target entity
|
||||||
orientation = std::atan2(dy, dx);
|
auto target = entityManager.getEntity(data.facingTarget);
|
||||||
}
|
if (target) {
|
||||||
}
|
float dx = target->getX() - entity->getX();
|
||||||
} else {
|
float dy = target->getY() - entity->getY();
|
||||||
// Normal move - face toward destination
|
|
||||||
float dx = destCanonical.x - entity->getX();
|
|
||||||
float dy = destCanonical.y - entity->getY();
|
|
||||||
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
orientation = std::atan2(dy, dx);
|
orientation = std::atan2(dy, dx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Interpolate entity position alongside renderer (so targeting matches visual)
|
// Normal move - face toward destination
|
||||||
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
float dx = destCanonical.x - entity->getX();
|
||||||
orientation, data.duration / 1000.0f);
|
float dy = destCanonical.y - entity->getY();
|
||||||
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
// Notify renderer to smoothly move the creature
|
orientation = std::atan2(dy, dx);
|
||||||
if (creatureMoveCallback_) {
|
|
||||||
creatureMoveCallback_(data.guid,
|
|
||||||
destCanonical.x, destCanonical.y, destCanonical.z,
|
|
||||||
data.duration);
|
|
||||||
}
|
}
|
||||||
} else if (data.moveType == 1) {
|
}
|
||||||
// Stop at current position
|
|
||||||
glm::vec3 posCanonical = core::coords::serverToCanonical(
|
|
||||||
glm::vec3(data.x, data.y, data.z));
|
|
||||||
entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z,
|
|
||||||
entity->getOrientation());
|
|
||||||
|
|
||||||
if (creatureMoveCallback_) {
|
// Interpolate entity position alongside renderer (so targeting matches visual)
|
||||||
creatureMoveCallback_(data.guid,
|
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
||||||
posCanonical.x, posCanonical.y, posCanonical.z, 0);
|
orientation, data.duration / 1000.0f);
|
||||||
}
|
|
||||||
|
// Notify renderer to smoothly move the creature
|
||||||
|
if (creatureMoveCallback_) {
|
||||||
|
creatureMoveCallback_(data.guid,
|
||||||
|
destCanonical.x, destCanonical.y, destCanonical.z,
|
||||||
|
data.duration);
|
||||||
|
}
|
||||||
|
} else if (data.moveType == 1) {
|
||||||
|
// Stop at current position
|
||||||
|
glm::vec3 posCanonical = core::coords::serverToCanonical(
|
||||||
|
glm::vec3(data.x, data.y, data.z));
|
||||||
|
entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z,
|
||||||
|
entity->getOrientation());
|
||||||
|
|
||||||
|
if (creatureMoveCallback_) {
|
||||||
|
creatureMoveCallback_(data.guid,
|
||||||
|
posCanonical.x, posCanonical.y, posCanonical.z, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2256,6 +2256,101 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& data) {
|
||||||
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (data.guid == 0) return false;
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
||||||
|
data.x = packet.readFloat();
|
||||||
|
data.y = packet.readFloat();
|
||||||
|
data.z = packet.readFloat();
|
||||||
|
|
||||||
|
// Turtle WoW movement payload uses a spline-style layout after XYZ:
|
||||||
|
// uint32 splineIdOrTick
|
||||||
|
// uint8 moveType
|
||||||
|
// [if moveType 2/3/4] facing payload
|
||||||
|
// uint32 splineFlags
|
||||||
|
// [if Animation] uint8 + uint32
|
||||||
|
// uint32 duration
|
||||||
|
// [if Parabolic] float + uint32
|
||||||
|
// uint32 pointCount
|
||||||
|
// float[3] dest
|
||||||
|
// uint32 packedPoints[pointCount-1]
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
/*uint32_t splineIdOrTick =*/ packet.readUInt32();
|
||||||
|
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
data.moveType = packet.readUInt8();
|
||||||
|
|
||||||
|
if (data.moveType == 1) {
|
||||||
|
data.destX = data.x;
|
||||||
|
data.destY = data.y;
|
||||||
|
data.destZ = data.z;
|
||||||
|
data.hasDest = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.moveType == 2) {
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
} else if (data.moveType == 3) {
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
data.facingTarget = packet.readUInt64();
|
||||||
|
} else if (data.moveType == 4) {
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.facingAngle = packet.readFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.splineFlags = packet.readUInt32();
|
||||||
|
|
||||||
|
// Animation flag (same bit as WotLK MoveSplineFlag::Animation)
|
||||||
|
if (data.splineFlags & 0x00400000) {
|
||||||
|
if (packet.getReadPos() + 5 > packet.getSize()) return false;
|
||||||
|
packet.readUInt8();
|
||||||
|
packet.readUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.duration = packet.readUInt32();
|
||||||
|
|
||||||
|
// Parabolic flag (same bit as WotLK MoveSplineFlag::Parabolic)
|
||||||
|
if (data.splineFlags & 0x00000800) {
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
packet.readFloat();
|
||||||
|
packet.readUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
uint32_t pointCount = packet.readUInt32();
|
||||||
|
|
||||||
|
if (pointCount == 0) return true;
|
||||||
|
if (pointCount > 16384) return false; // sanity
|
||||||
|
|
||||||
|
// First float[3] is destination.
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
data.destX = packet.readFloat();
|
||||||
|
data.destY = packet.readFloat();
|
||||||
|
data.destZ = packet.readFloat();
|
||||||
|
data.hasDest = true;
|
||||||
|
|
||||||
|
// Remaining waypoints are packed as uint32 deltas.
|
||||||
|
if (pointCount > 1) {
|
||||||
|
size_t skipBytes = static_cast<size_t>(pointCount - 1) * 4;
|
||||||
|
size_t newPos = packet.getReadPos() + skipBytes;
|
||||||
|
if (newPos <= packet.getSize()) {
|
||||||
|
packet.setReadPos(newPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,
|
||||||
|
" type=", (int)data.moveType, " dur=", data.duration, "ms",
|
||||||
|
" dest=(", data.destX, ",", data.destY, ",", data.destZ, ")");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Phase 2: Combat Core
|
// Phase 2: Combat Core
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue