mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Harden packet framing/logging and checkpoint current workspace state
This commit is contained in:
parent
4b48fcdab2
commit
615efd01b7
9 changed files with 290 additions and 147 deletions
|
|
@ -58,6 +58,8 @@ struct ActiveTransport {
|
|||
bool clientAnimationReverse; // Run client animation in reverse along the selected path
|
||||
float serverYaw; // Server-authoritative yaw (radians)
|
||||
bool hasServerYaw; // Whether we've received server yaw
|
||||
bool serverYawFlipped180; // Auto-correction when server yaw is consistently opposite movement
|
||||
int serverYawAlignmentScore; // Hysteresis score for yaw flip detection
|
||||
|
||||
float lastServerUpdate; // Time of last server movement update
|
||||
int serverUpdateCount; // Number of server updates received
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ namespace network {
|
|||
*
|
||||
* Key Differences from Auth Server:
|
||||
* - Outgoing: 6-byte header (2 bytes size + 4 bytes opcode, big-endian)
|
||||
* - Incoming: 4-byte header (2 bytes size + 2 bytes opcode, big-endian)
|
||||
* - Incoming: 4-byte header (2 bytes size + 2 bytes opcode)
|
||||
* - Headers are RC4-encrypted after CMSG_AUTH_SESSION
|
||||
* - Packet bodies remain unencrypted
|
||||
* - Size field is payload size only (does NOT include header)
|
||||
* - Size field includes opcode bytes (payloadLen = size - 2)
|
||||
*/
|
||||
class WorldSocket : public Socket {
|
||||
public:
|
||||
|
|
@ -89,6 +89,9 @@ private:
|
|||
// This prevents re-decrypting the same header when waiting for more data
|
||||
size_t headerBytesDecrypted = 0;
|
||||
|
||||
// Debug-only tracing window for post-auth packet framing verification.
|
||||
int headerTracePacketsLeft = 0;
|
||||
|
||||
// Packet callback
|
||||
std::function<void(const Packet&)> packetCallback;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@ public:
|
|||
*/
|
||||
std::vector<uint8_t> readFile(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Read optional file data from MPQ archives without warning spam.
|
||||
* Intended for probe-style lookups (e.g. external .anim variants).
|
||||
* @param path Virtual file path
|
||||
* @return File contents (empty if not found)
|
||||
*/
|
||||
std::vector<uint8_t> readFileOptional(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Get MPQ manager for direct access
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1209,20 +1209,12 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
|
||||
if (preferServerData) {
|
||||
// Server-first mode: keep authoritative server snapshots, but still choose a
|
||||
// deterministic DBC path (entry/remap) as fallback if updates go stale.
|
||||
// Strict server-authoritative mode: do not infer/remap fallback routes.
|
||||
if (!hasUsablePath) {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Server-first transport registration: fallback path ", pathId,
|
||||
" for entry ", entry, " displayId=", displayId);
|
||||
} else {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_INFO("Server-first transport registration: stationary fallback for GUID 0x",
|
||||
std::hex, guid, std::dec, " entry=", entry);
|
||||
}
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_INFO("Server-first strict registration: stationary fallback for GUID 0x",
|
||||
std::hex, guid, std::dec, " entry=", entry);
|
||||
} else {
|
||||
LOG_INFO("Server-first transport registration: using entry DBC path for entry ", entry);
|
||||
}
|
||||
|
|
@ -1316,19 +1308,12 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
|
||||
if (preferServerData) {
|
||||
// Strict server-authoritative mode: no inferred/remapped fallback routes.
|
||||
if (!hasUsablePath) {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_INFO("Auto-spawned transport in server-first mode with fallback path: entry=", entry,
|
||||
" remappedPath=", pathId, " displayId=", displayId,
|
||||
" wmoInstance=", wmoInstanceId);
|
||||
} else {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_INFO("Auto-spawned transport in server-first mode (stationary fallback): entry=", entry,
|
||||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
}
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_INFO("Auto-spawned transport in strict server-first mode (stationary fallback): entry=", entry,
|
||||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
} else {
|
||||
LOG_INFO("Auto-spawned transport in server-first mode with entry DBC path: entry=", entry,
|
||||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
|
|
@ -1711,7 +1696,7 @@ void Application::spawnPlayerCharacter() {
|
|||
baseName.c_str(),
|
||||
model.sequences[si].id,
|
||||
model.sequences[si].variationIndex);
|
||||
auto animFileData = assetManager->readFile(animFileName);
|
||||
auto animFileData = assetManager->readFileOptional(animFileName);
|
||||
if (!animFileData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
|
||||
}
|
||||
|
|
@ -2749,7 +2734,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFile(animFileName);
|
||||
auto animData = assetManager->readFileOptional(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
|
|
@ -3639,7 +3624,7 @@ void Application::processPendingMount() {
|
|||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), animId, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFile(animFileName);
|
||||
auto animData = assetManager->readFileOptional(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,42 @@
|
|||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
namespace {
|
||||
const char* worldStateName(WorldState state) {
|
||||
switch (state) {
|
||||
case WorldState::DISCONNECTED: return "DISCONNECTED";
|
||||
case WorldState::CONNECTING: return "CONNECTING";
|
||||
case WorldState::CONNECTED: return "CONNECTED";
|
||||
case WorldState::CHALLENGE_RECEIVED: return "CHALLENGE_RECEIVED";
|
||||
case WorldState::AUTH_SENT: return "AUTH_SENT";
|
||||
case WorldState::AUTHENTICATED: return "AUTHENTICATED";
|
||||
case WorldState::READY: return "READY";
|
||||
case WorldState::CHAR_LIST_REQUESTED: return "CHAR_LIST_REQUESTED";
|
||||
case WorldState::CHAR_LIST_RECEIVED: return "CHAR_LIST_RECEIVED";
|
||||
case WorldState::ENTERING_WORLD: return "ENTERING_WORLD";
|
||||
case WorldState::IN_WORLD: return "IN_WORLD";
|
||||
case WorldState::FAILED: return "FAILED";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool isAuthCharPipelineOpcode(uint16_t opcode) {
|
||||
switch (opcode) {
|
||||
case static_cast<uint16_t>(Opcode::SMSG_AUTH_CHALLENGE):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_AUTH_RESPONSE):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CLIENTCACHE_VERSION):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_TUTORIAL_FLAGS):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CHAR_ENUM):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CHAR_CREATE):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CHAR_DELETE):
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
GameHandler::GameHandler() {
|
||||
LOG_DEBUG("GameHandler created");
|
||||
|
|
@ -171,7 +207,7 @@ void GameHandler::update(float deltaTime) {
|
|||
socketTime += std::chrono::duration<float, std::milli>(socketEnd - socketStart).count();
|
||||
|
||||
// Post-gate visibility: determine whether server goes silent or closes after Warden requirement.
|
||||
if (wardenGateSeen_ && socket) {
|
||||
if (wardenGateSeen_ && socket && socket->isConnected()) {
|
||||
wardenGateElapsed_ += deltaTime;
|
||||
if (wardenGateElapsed_ >= wardenGateNextStatusLog_) {
|
||||
LOG_INFO("Warden gate status: elapsed=", wardenGateElapsed_,
|
||||
|
|
@ -504,6 +540,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (wardenGateSeen_ && opcode != static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA)) {
|
||||
++wardenPacketsAfterGate_;
|
||||
}
|
||||
if (isAuthCharPipelineOpcode(opcode)) {
|
||||
LOG_INFO("AUTH/CHAR RX opcode=0x", std::hex, opcode, std::dec,
|
||||
" state=", worldStateName(state),
|
||||
" size=", packet.getSize());
|
||||
}
|
||||
|
||||
LOG_DEBUG("Received world packet: opcode=0x", std::hex, opcode, std::dec,
|
||||
" size=", packet.getSize(), " bytes");
|
||||
|
|
@ -516,7 +557,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (state == WorldState::CONNECTED) {
|
||||
handleAuthChallenge(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_AUTH_CHALLENGE in state: ", (int)state);
|
||||
LOG_WARNING("Unexpected SMSG_AUTH_CHALLENGE in state: ", worldStateName(state));
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -524,7 +565,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (state == WorldState::AUTH_SENT) {
|
||||
handleAuthResponse(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_AUTH_RESPONSE in state: ", (int)state);
|
||||
LOG_WARNING("Unexpected SMSG_AUTH_RESPONSE in state: ", worldStateName(state));
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -546,7 +587,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (state == WorldState::CHAR_LIST_REQUESTED) {
|
||||
handleCharEnum(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_CHAR_ENUM in state: ", (int)state);
|
||||
LOG_WARNING("Unexpected SMSG_CHAR_ENUM in state: ", worldStateName(state));
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -554,7 +595,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (state == WorldState::ENTERING_WORLD || state == WorldState::IN_WORLD) {
|
||||
handleLoginVerifyWorld(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", (int)state);
|
||||
LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", worldStateName(state));
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -1433,7 +1474,7 @@ void GameHandler::requestCharacterList() {
|
|||
|
||||
if (state != WorldState::READY && state != WorldState::AUTHENTICATED &&
|
||||
state != WorldState::CHAR_LIST_RECEIVED) {
|
||||
LOG_WARNING("Cannot request character list in state: ", (int)state);
|
||||
LOG_WARNING("Cannot request character list in state: ", worldStateName(state));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1507,7 +1548,7 @@ void GameHandler::createCharacter(const CharCreateData& data) {
|
|||
|
||||
if (state != WorldState::CHAR_LIST_RECEIVED) {
|
||||
std::string msg = "Character list not ready yet. Wait for SMSG_CHAR_ENUM.";
|
||||
LOG_WARNING("Blocking CMSG_CHAR_CREATE in state=", static_cast<int>(state),
|
||||
LOG_WARNING("Blocking CMSG_CHAR_CREATE in state=", worldStateName(state),
|
||||
" (awaiting CHAR_LIST_RECEIVED)");
|
||||
if (charCreateCallback_) {
|
||||
charCreateCallback_(false, msg);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
|
@ -43,7 +44,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.guid = guid;
|
||||
transport.wmoInstanceId = wmoInstanceId;
|
||||
transport.pathId = pathId;
|
||||
transport.allowBootstrapVelocity = true;
|
||||
transport.allowBootstrapVelocity = false;
|
||||
|
||||
// CRITICAL: Set basePosition from spawn position and t=0 offset
|
||||
// For stationary paths (1 waypoint), just use spawn position directly
|
||||
|
|
@ -79,6 +80,8 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.clientAnimationReverse = false;
|
||||
transport.serverYaw = 0.0f;
|
||||
transport.hasServerYaw = false;
|
||||
transport.serverYawFlipped180 = false;
|
||||
transport.serverYawAlignmentScore = 0;
|
||||
transport.lastServerUpdate = 0.0f;
|
||||
transport.serverUpdateCount = 0;
|
||||
transport.serverLinearVelocity = glm::vec3(0.0f);
|
||||
|
|
@ -254,16 +257,7 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
}
|
||||
pathTimeMs = transport.localClockMs % path.durationMs;
|
||||
} else {
|
||||
// Server-driven transport without clock sync:
|
||||
// keep server-authoritative and dead-reckon from last known velocity.
|
||||
const float ageSec = elapsedTime_ - transport.lastServerUpdate;
|
||||
constexpr float kMaxExtrapolationSec = 30.0f;
|
||||
|
||||
if (transport.hasServerVelocity && ageSec > 0.0f && ageSec <= kMaxExtrapolationSec) {
|
||||
const float blend = glm::clamp(1.0f - (ageSec / kMaxExtrapolationSec), 0.0f, 1.0f);
|
||||
transport.position += transport.serverLinearVelocity * (deltaTime * blend);
|
||||
}
|
||||
|
||||
// Strict server-authoritative mode: do not guess movement between server snapshots.
|
||||
updateTransformMatrices(transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
|
|
@ -279,11 +273,17 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
|
||||
}
|
||||
if (!transport.useClientAnimation && !transport.hasServerClock) {
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
constexpr float kMaxFallbackZOffset = 8.0f;
|
||||
pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset);
|
||||
}
|
||||
transport.position = transport.basePosition + pathOffset;
|
||||
|
||||
// Use server yaw if available (authoritative), otherwise compute from tangent
|
||||
if (transport.hasServerYaw) {
|
||||
transport.rotation = glm::angleAxis(transport.serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
float effectiveYaw = transport.serverYaw + (transport.serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport.rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
} else {
|
||||
transport.rotation = orientationFromTangent(path, pathTimeMs);
|
||||
}
|
||||
|
|
@ -560,7 +560,8 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
transport->position = position;
|
||||
transport->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
transport->rotation = glm::angleAxis(transport->serverYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
float effectiveYaw = transport->serverYaw + (transport->serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport->rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
||||
if (hadPrevUpdate) {
|
||||
const float dt = elapsedTime_ - prevUpdateTime;
|
||||
|
|
@ -570,6 +571,35 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
constexpr float kMinAuthoritativeSpeed = 0.15f;
|
||||
constexpr float kMaxSpeed = 60.0f;
|
||||
if (speed >= kMinAuthoritativeSpeed) {
|
||||
// Auto-detect 180-degree yaw mismatch by comparing heading to movement direction.
|
||||
// Some transports appear to report yaw opposite their actual travel direction.
|
||||
glm::vec2 horizontalV(v.x, v.y);
|
||||
float hLen = glm::length(horizontalV);
|
||||
if (hLen > 0.2f) {
|
||||
horizontalV /= hLen;
|
||||
glm::vec2 heading(std::cos(transport->serverYaw), std::sin(transport->serverYaw));
|
||||
float alignDot = glm::dot(heading, horizontalV);
|
||||
|
||||
if (alignDot < -0.35f) {
|
||||
transport->serverYawAlignmentScore = std::max(transport->serverYawAlignmentScore - 1, -12);
|
||||
} else if (alignDot > 0.35f) {
|
||||
transport->serverYawAlignmentScore = std::min(transport->serverYawAlignmentScore + 1, 12);
|
||||
}
|
||||
|
||||
if (!transport->serverYawFlipped180 && transport->serverYawAlignmentScore <= -4) {
|
||||
transport->serverYawFlipped180 = true;
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" enabled 180-degree yaw correction (alignScore=",
|
||||
transport->serverYawAlignmentScore, ")");
|
||||
} else if (transport->serverYawFlipped180 &&
|
||||
transport->serverYawAlignmentScore >= 4) {
|
||||
transport->serverYawFlipped180 = false;
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" disabled 180-degree yaw correction (alignScore=",
|
||||
transport->serverYawAlignmentScore, ")");
|
||||
}
|
||||
}
|
||||
|
||||
if (speed > kMaxSpeed) {
|
||||
v *= (kMaxSpeed / speed);
|
||||
}
|
||||
|
|
@ -577,12 +607,36 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
transport->serverLinearVelocity = v;
|
||||
transport->serverAngularVelocity = 0.0f;
|
||||
transport->hasServerVelocity = true;
|
||||
|
||||
// Re-apply potentially corrected yaw this frame after alignment check.
|
||||
effectiveYaw = transport->serverYaw + (transport->serverYawFlipped180 ? glm::pi<float>() : 0.0f);
|
||||
transport->rotation = glm::angleAxis(effectiveYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Seed fallback path phase from nearest waypoint to the first authoritative sample.
|
||||
auto pathIt2 = paths_.find(transport->pathId);
|
||||
if (pathIt2 != paths_.end()) {
|
||||
const auto& path = pathIt2->second;
|
||||
if (!path.points.empty() && path.durationMs > 0) {
|
||||
glm::vec3 local = position - transport->basePosition;
|
||||
size_t bestIdx = 0;
|
||||
float bestDistSq = std::numeric_limits<float>::max();
|
||||
for (size_t i = 0; i < path.points.size(); ++i) {
|
||||
glm::vec3 d = path.points[i].pos - local;
|
||||
float distSq = glm::dot(d, d);
|
||||
if (distSq < bestDistSq) {
|
||||
bestDistSq = distSq;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
transport->localClockMs = path.points[bestIdx].tMs % path.durationMs;
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap velocity from mapped DBC path on first authoritative sample.
|
||||
// This avoids "stalled at dock" when server sends sparse transport snapshots.
|
||||
auto pathIt2 = paths_.find(transport->pathId);
|
||||
pathIt2 = paths_.find(transport->pathId);
|
||||
if (transport->allowBootstrapVelocity && pathIt2 != paths_.end()) {
|
||||
const auto& path = pathIt2->second;
|
||||
if (path.points.size() >= 2 && path.durationMs > 0) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,40 @@
|
|||
#include "core/logger.hpp"
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
|
||||
|
||||
inline bool isLoginPipelineSmsg(uint16_t opcode) {
|
||||
switch (opcode) {
|
||||
case 0x1EC: // SMSG_AUTH_CHALLENGE
|
||||
case 0x1EE: // SMSG_AUTH_RESPONSE
|
||||
case 0x03B: // SMSG_CHAR_ENUM
|
||||
case 0x03A: // SMSG_CHAR_CREATE
|
||||
case 0x03C: // SMSG_CHAR_DELETE
|
||||
case 0x4AB: // SMSG_CLIENTCACHE_VERSION
|
||||
case 0x0FD: // SMSG_TUTORIAL_FLAGS
|
||||
case 0x2E6: // SMSG_WARDEN_DATA
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool isLoginPipelineCmsg(uint16_t opcode) {
|
||||
switch (opcode) {
|
||||
case 0x1ED: // CMSG_AUTH_SESSION
|
||||
case 0x037: // CMSG_CHAR_ENUM
|
||||
case 0x036: // CMSG_CHAR_CREATE
|
||||
case 0x038: // CMSG_CHAR_DELETE
|
||||
case 0x03D: // CMSG_PLAYER_LOGIN
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace wowee {
|
||||
namespace network {
|
||||
|
|
@ -127,23 +161,6 @@ void WorldSocket::send(const Packet& packet) {
|
|||
// Add payload (unencrypted)
|
||||
sendData.insert(sendData.end(), data.begin(), data.end());
|
||||
|
||||
|
||||
// Debug: dump first few movement packets
|
||||
{
|
||||
static int moveDump = 3;
|
||||
bool isMove = (opcode >= 0xB5 && opcode <= 0xBE) || opcode == 0xC9 || opcode == 0xDA || opcode == 0xEE;
|
||||
if (isMove && moveDump-- > 0) {
|
||||
std::string hex = "MOVE PKT dump opcode=0x";
|
||||
char buf[8]; snprintf(buf, sizeof(buf), "%03x", opcode); hex += buf;
|
||||
hex += " payload=" + std::to_string(payloadLen) + " bytes: ";
|
||||
for (size_t i = 6; i < sendData.size() && i < 6 + 48; i++) {
|
||||
char b[4]; snprintf(b, sizeof(b), "%02x ", sendData[i]);
|
||||
hex += b;
|
||||
}
|
||||
LOG_INFO(hex);
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: dump packet bytes for AUTH_SESSION
|
||||
if (opcode == 0x1ED) {
|
||||
std::string hexDump = "AUTH_SESSION raw bytes: ";
|
||||
|
|
@ -155,6 +172,10 @@ void WorldSocket::send(const Packet& packet) {
|
|||
}
|
||||
LOG_DEBUG(hexDump);
|
||||
}
|
||||
if (isLoginPipelineCmsg(opcode)) {
|
||||
LOG_INFO("WS TX LOGIN opcode=0x", std::hex, opcode, std::dec,
|
||||
" payload=", payloadLen, " enc=", encryptionEnabled ? "yes" : "no");
|
||||
}
|
||||
|
||||
// Send complete packet
|
||||
ssize_t sent = net::portableSend(sockfd, sendData.data(), sendData.size());
|
||||
|
|
@ -170,26 +191,48 @@ void WorldSocket::send(const Packet& packet) {
|
|||
void WorldSocket::update() {
|
||||
if (!connected) return;
|
||||
|
||||
// Receive data into buffer
|
||||
uint8_t buffer[4096];
|
||||
ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer));
|
||||
size_t bytesReadThisTick = 0;
|
||||
int readOps = 0;
|
||||
while (connected) {
|
||||
uint8_t buffer[4096];
|
||||
ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer));
|
||||
|
||||
if (received > 0) {
|
||||
LOG_DEBUG("Received ", received, " bytes from world server");
|
||||
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
|
||||
if (received > 0) {
|
||||
++readOps;
|
||||
bytesReadThisTick += static_cast<size_t>(received);
|
||||
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
|
||||
if (receiveBuffer.size() > kMaxReceiveBufferBytes) {
|
||||
LOG_ERROR("World socket receive buffer overflow (", receiveBuffer.size(),
|
||||
" bytes). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to parse complete packets from buffer
|
||||
tryParsePackets();
|
||||
}
|
||||
else if (received == 0) {
|
||||
LOG_INFO("World server connection closed");
|
||||
disconnect();
|
||||
}
|
||||
else {
|
||||
int err = net::lastError();
|
||||
if (!net::isWouldBlock(err)) {
|
||||
LOG_ERROR("Receive failed: ", net::errorString(err));
|
||||
if (received == 0) {
|
||||
LOG_INFO("World server connection closed");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
int err = net::lastError();
|
||||
if (net::isWouldBlock(err)) {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_ERROR("Receive failed: ", net::errorString(err));
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bytesReadThisTick > 0) {
|
||||
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
|
||||
" recv call(s), buffered=", receiveBuffer.size());
|
||||
tryParsePackets();
|
||||
if (connected && !receiveBuffer.empty()) {
|
||||
LOG_DEBUG("World socket parse left ", receiveBuffer.size(),
|
||||
" bytes buffered (awaiting complete packet)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,6 +240,9 @@ void WorldSocket::update() {
|
|||
void WorldSocket::tryParsePackets() {
|
||||
// World server packets have 4-byte incoming header: size(2) + opcode(2)
|
||||
while (receiveBuffer.size() >= 4) {
|
||||
uint8_t rawHeader[4] = {0, 0, 0, 0};
|
||||
std::memcpy(rawHeader, receiveBuffer.data(), 4);
|
||||
|
||||
// Decrypt header bytes in-place if encryption is enabled
|
||||
// Only decrypt bytes we haven't already decrypted
|
||||
if (encryptionEnabled && headerBytesDecrypted < 4) {
|
||||
|
|
@ -205,48 +251,62 @@ void WorldSocket::tryParsePackets() {
|
|||
headerBytesDecrypted = 4;
|
||||
}
|
||||
|
||||
// Parse header (now decrypted in-place)
|
||||
// Size: 2 bytes big-endian (includes opcode, so payload = size - 2)
|
||||
// Parse header (now decrypted in-place).
|
||||
// Size: 2 bytes big-endian. For world packets, this includes opcode bytes.
|
||||
uint16_t size = (receiveBuffer[0] << 8) | receiveBuffer[1];
|
||||
// Opcode: 2 bytes little-endian
|
||||
// Opcode: 2 bytes little-endian.
|
||||
uint16_t opcode = receiveBuffer[2] | (receiveBuffer[3] << 8);
|
||||
if (size < 2) {
|
||||
LOG_ERROR("World packet framing desync: invalid size=", size,
|
||||
" rawHdr=", std::hex,
|
||||
static_cast<int>(rawHeader[0]), " ",
|
||||
static_cast<int>(rawHeader[1]), " ",
|
||||
static_cast<int>(rawHeader[2]), " ",
|
||||
static_cast<int>(rawHeader[3]), std::dec,
|
||||
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
constexpr uint16_t kMaxWorldPacketSize = 0x4000;
|
||||
if (size > kMaxWorldPacketSize) {
|
||||
LOG_ERROR("World packet framing desync: oversized packet size=", size,
|
||||
" rawHdr=", std::hex,
|
||||
static_cast<int>(rawHeader[0]), " ",
|
||||
static_cast<int>(rawHeader[1]), " ",
|
||||
static_cast<int>(rawHeader[2]), " ",
|
||||
static_cast<int>(rawHeader[3]), std::dec,
|
||||
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Total packet size: size field (2) + size value (which includes opcode + payload)
|
||||
size_t totalSize = 2 + size;
|
||||
const uint16_t payloadLen = size - 2;
|
||||
const size_t totalSize = 4 + payloadLen;
|
||||
|
||||
// DEBUG: Log packet boundary details for quest-related opcodes
|
||||
if (opcode == 0x18F || opcode == 0x18D || opcode == 0x188 || opcode == 0x186) {
|
||||
char hexBuf[256];
|
||||
snprintf(hexBuf, sizeof(hexBuf),
|
||||
"PACKET BOUNDARY: opcode=0x%04X size=%u totalSize=%zu bufferSize=%zu",
|
||||
opcode, size, totalSize, receiveBuffer.size());
|
||||
core::Logger::getInstance().info(hexBuf);
|
||||
|
||||
// Dump header bytes
|
||||
snprintf(hexBuf, sizeof(hexBuf),
|
||||
" Header: %02x %02x %02x %02x",
|
||||
receiveBuffer[0], receiveBuffer[1], receiveBuffer[2], receiveBuffer[3]);
|
||||
core::Logger::getInstance().info(hexBuf);
|
||||
|
||||
// Dump first 16 bytes of payload (if available)
|
||||
if (totalSize <= receiveBuffer.size()) {
|
||||
std::string payloadHex = " Payload: ";
|
||||
for (size_t i = 4; i < std::min(totalSize, size_t(20)); ++i) {
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]);
|
||||
payloadHex += buf;
|
||||
}
|
||||
core::Logger::getInstance().info(payloadHex);
|
||||
}
|
||||
|
||||
// Dump what comes after this packet (next header preview)
|
||||
if (receiveBuffer.size() > totalSize && receiveBuffer.size() >= totalSize + 4) {
|
||||
snprintf(hexBuf, sizeof(hexBuf),
|
||||
" Next header: %02x %02x %02x %02x",
|
||||
receiveBuffer[totalSize], receiveBuffer[totalSize+1],
|
||||
receiveBuffer[totalSize+2], receiveBuffer[totalSize+3]);
|
||||
core::Logger::getInstance().info(hexBuf);
|
||||
}
|
||||
if (headerTracePacketsLeft > 0) {
|
||||
LOG_INFO("WS HDR TRACE raw=",
|
||||
std::hex,
|
||||
static_cast<int>(rawHeader[0]), " ",
|
||||
static_cast<int>(rawHeader[1]), " ",
|
||||
static_cast<int>(rawHeader[2]), " ",
|
||||
static_cast<int>(rawHeader[3]),
|
||||
" dec=",
|
||||
static_cast<int>(receiveBuffer[0]), " ",
|
||||
static_cast<int>(receiveBuffer[1]), " ",
|
||||
static_cast<int>(receiveBuffer[2]), " ",
|
||||
static_cast<int>(receiveBuffer[3]),
|
||||
std::dec,
|
||||
" size=", size,
|
||||
" payload=", payloadLen,
|
||||
" opcode=0x", std::hex, opcode, std::dec,
|
||||
" buffered=", receiveBuffer.size());
|
||||
--headerTracePacketsLeft;
|
||||
}
|
||||
if (isLoginPipelineSmsg(opcode)) {
|
||||
LOG_INFO("WS RX LOGIN opcode=0x", std::hex, opcode, std::dec,
|
||||
" size=", size, " payload=", payloadLen,
|
||||
" buffered=", receiveBuffer.size(),
|
||||
" enc=", encryptionEnabled ? "yes" : "no");
|
||||
}
|
||||
|
||||
if (receiveBuffer.size() < totalSize) {
|
||||
|
|
@ -261,29 +321,6 @@ void WorldSocket::tryParsePackets() {
|
|||
// Create packet with opcode and payload
|
||||
Packet packet(opcode, packetData);
|
||||
|
||||
// Log raw SMSG_TALENTS_INFO packets at network boundary
|
||||
if (opcode == 0x4C0) { // SMSG_TALENTS_INFO
|
||||
std::stringstream headerHex, payloadHex;
|
||||
headerHex << std::hex << std::setfill('0');
|
||||
payloadHex << std::hex << std::setfill('0');
|
||||
|
||||
// Header (4 bytes from receiveBuffer before packetData extraction)
|
||||
// Note: receiveBuffer still has the full packet at this point
|
||||
for (size_t i = 0; i < 4 && i < receiveBuffer.size(); ++i) {
|
||||
headerHex << std::setw(2) << (int)(uint8_t)receiveBuffer[i] << " ";
|
||||
}
|
||||
|
||||
// Payload (ALL bytes)
|
||||
for (size_t i = 0; i < packetData.size(); ++i) {
|
||||
payloadHex << std::setw(2) << (int)(uint8_t)packetData[i] << " ";
|
||||
}
|
||||
|
||||
LOG_INFO("=== SMSG_TALENTS_INFO RAW PACKET ===");
|
||||
LOG_INFO("Header: ", headerHex.str());
|
||||
LOG_INFO("Payload: ", payloadHex.str());
|
||||
LOG_INFO("Total payload size: ", packetData.size(), " bytes");
|
||||
}
|
||||
|
||||
// Remove parsed data from buffer and reset header decryption counter
|
||||
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize);
|
||||
headerBytesDecrypted = 0;
|
||||
|
|
@ -324,6 +361,7 @@ void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey) {
|
|||
decryptCipher.drop(1024);
|
||||
|
||||
encryptionEnabled = true;
|
||||
headerTracePacketsLeft = 24;
|
||||
LOG_INFO("World server encryption initialized successfully");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,18 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
|
|||
return data;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AssetManager::readFileOptional(const std::string& path) const {
|
||||
if (!initialized) {
|
||||
return std::vector<uint8_t>();
|
||||
}
|
||||
|
||||
// Avoid MPQManager missing-file warnings for expected probe misses.
|
||||
if (!fileExists(path)) {
|
||||
return std::vector<uint8_t>();
|
||||
}
|
||||
return readFile(path);
|
||||
}
|
||||
|
||||
void AssetManager::clearCache() {
|
||||
std::scoped_lock lock(readMutex, cacheMutex);
|
||||
dbcCache.clear();
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
baseName.c_str(),
|
||||
model.sequences[si].id,
|
||||
model.sequences[si].variationIndex);
|
||||
auto animFileData = assetManager_->readFile(animFileName);
|
||||
auto animFileData = assetManager_->readFileOptional(animFileName);
|
||||
if (!animFileData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue