From 615efd01b772f46bdd3b261cb145936bf84a22c0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Feb 2026 02:27:59 -0800 Subject: [PATCH] Harden packet framing/logging and checkpoint current workspace state --- include/game/transport_manager.hpp | 2 + include/network/world_socket.hpp | 7 +- include/pipeline/asset_manager.hpp | 8 + src/core/application.cpp | 41 ++--- src/game/game_handler.cpp | 55 ++++++- src/game/transport_manager.cpp | 82 ++++++++-- src/network/world_socket.cpp | 228 ++++++++++++++++------------ src/pipeline/asset_manager.cpp | 12 ++ src/rendering/character_preview.cpp | 2 +- 9 files changed, 290 insertions(+), 147 deletions(-) diff --git a/include/game/transport_manager.hpp b/include/game/transport_manager.hpp index 42842e13..53f9e5c9 100644 --- a/include/game/transport_manager.hpp +++ b/include/game/transport_manager.hpp @@ -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 diff --git a/include/network/world_socket.hpp b/include/network/world_socket.hpp index 86f869c6..7ffd1515 100644 --- a/include/network/world_socket.hpp +++ b/include/network/world_socket.hpp @@ -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 packetCallback; }; diff --git a/include/pipeline/asset_manager.hpp b/include/pipeline/asset_manager.hpp index 6222a017..ba69b75d 100644 --- a/include/pipeline/asset_manager.hpp +++ b/include/pipeline/asset_manager.hpp @@ -73,6 +73,14 @@ public: */ std::vector 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 readFileOptional(const std::string& path) const; + /** * Get MPQ manager for direct access */ diff --git a/src/core/application.cpp b/src/core/application.cpp index f29d91db..540d648c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 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 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 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 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); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f398e8bf..cb5df1a5 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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(Opcode::SMSG_AUTH_CHALLENGE): + case static_cast(Opcode::SMSG_AUTH_RESPONSE): + case static_cast(Opcode::SMSG_CLIENTCACHE_VERSION): + case static_cast(Opcode::SMSG_TUTORIAL_FLAGS): + case static_cast(Opcode::SMSG_WARDEN_DATA): + case static_cast(Opcode::SMSG_CHAR_ENUM): + case static_cast(Opcode::SMSG_CHAR_CREATE): + case static_cast(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(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(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(state), + LOG_WARNING("Blocking CMSG_CHAR_CREATE in state=", worldStateName(state), " (awaiting CHAR_LIST_RECEIVED)"); if (charCreateCallback_) { charCreateCallback_(false, msg); diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 0fcf01c3..5c63c5a8 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -5,6 +5,7 @@ #include "pipeline/dbc_loader.hpp" #include "pipeline/asset_manager.hpp" #include +#include #include #include #include @@ -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() : 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() : 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() : 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::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) { diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index d281b135..b1de11ef 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -5,6 +5,40 @@ #include "core/logger.hpp" #include #include +#include + +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(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(rawHeader[0]), " ", + static_cast(rawHeader[1]), " ", + static_cast(rawHeader[2]), " ", + static_cast(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(rawHeader[0]), " ", + static_cast(rawHeader[1]), " ", + static_cast(rawHeader[2]), " ", + static_cast(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(rawHeader[0]), " ", + static_cast(rawHeader[1]), " ", + static_cast(rawHeader[2]), " ", + static_cast(rawHeader[3]), + " dec=", + static_cast(receiveBuffer[0]), " ", + static_cast(receiveBuffer[1]), " ", + static_cast(receiveBuffer[2]), " ", + static_cast(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& sessionKey) { decryptCipher.drop(1024); encryptionEnabled = true; + headerTracePacketsLeft = 24; LOG_INFO("World server encryption initialized successfully"); } diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 0522f9fb..bce7bd1a 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -214,6 +214,18 @@ std::vector AssetManager::readFile(const std::string& path) const { return data; } +std::vector AssetManager::readFileOptional(const std::string& path) const { + if (!initialized) { + return std::vector(); + } + + // Avoid MPQManager missing-file warnings for expected probe misses. + if (!fileExists(path)) { + return std::vector(); + } + return readFile(path); +} + void AssetManager::clearCache() { std::scoped_lock lock(readMutex, cacheMutex); dbcCache.clear(); diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index bccdb6a7..13e828ef 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -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); }