diff --git a/include/core/application.hpp b/include/core/application.hpp index 38f817b6..acc58960 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace wowee { @@ -81,6 +82,10 @@ private: std::string getPlayerModelPath() const; static const char* mapIdToName(uint32_t mapId); void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); + void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation); + void despawnOnlineCreature(uint64_t guid); + void buildCreatureDisplayLookups(); + std::string getModelPathForDisplayId(uint32_t displayId) const; static Application* instance; @@ -118,6 +123,14 @@ private: std::vector underwearPaths_; uint32_t skinTextureSlotIndex_ = 0; uint32_t cloakTextureSlotIndex_ = 0; + + // Online creature model spawning + std::unordered_map displayToModelId_; // displayId → modelId (from CreatureDisplayInfo.dbc) + std::unordered_map modelIdToPath_; // modelId → M2 path (from CreatureModelData.dbc) + std::unordered_map creatureInstances_; // guid → render instanceId + std::unordered_map creatureModelIds_; // guid → loaded modelId + uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures + bool creatureLookupsBuilt_ = false; }; } // namespace core diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 0de47d8d..14da2aa2 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -147,6 +147,10 @@ public: uint32_t getEntry() const { return entry; } void setEntry(uint32_t e) { entry = e; } + // Display ID (model display) + uint32_t getDisplayId() const { return displayId; } + void setDisplayId(uint32_t id) { displayId = id; } + protected: std::string name; uint32_t health = 0; @@ -156,6 +160,7 @@ protected: uint8_t powerType = 0; // 0=mana, 1=rage, 2=focus, 3=energy uint32_t level = 1; uint32_t entry = 0; + uint32_t displayId = 0; }; /** diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2bcd50f1..770a42a9 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -276,6 +276,15 @@ public: using WorldEntryCallback = std::function; void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); } + // Creature spawn callback (online mode - triggered when creature enters view) + // Parameters: guid, displayId, x, y, z (canonical), orientation + using CreatureSpawnCallback = std::function; + void setCreatureSpawnCallback(CreatureSpawnCallback cb) { creatureSpawnCallback_ = std::move(cb); } + + // Creature despawn callback (online mode - triggered when creature leaves view) + using CreatureDespawnCallback = std::function; + void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); } + // Cooldowns float getSpellCooldown(uint32_t spellId) const; @@ -383,6 +392,11 @@ private: */ void handleUpdateObject(network::Packet& packet); + /** + * Handle SMSG_COMPRESSED_UPDATE_OBJECT from server + */ + void handleCompressedUpdateObject(network::Packet& packet); + /** * Handle SMSG_DESTROY_OBJECT from server */ @@ -529,6 +543,8 @@ private: // ---- Phase 3: Spells ---- HearthstoneCallback hearthstoneCallback; WorldEntryCallback worldEntryCallback_; + CreatureSpawnCallback creatureSpawnCallback_; + CreatureDespawnCallback creatureDespawnCallback_; std::vector knownSpells; std::unordered_map spellCooldowns; // spellId -> remaining seconds uint8_t castCount = 0; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 5d31dbb6..ada6889b 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -46,6 +46,7 @@ enum class Opcode : uint16_t { // ---- Entity/Object updates ---- SMSG_UPDATE_OBJECT = 0x0A9, + SMSG_COMPRESSED_UPDATE_OBJECT = 0x1F6, SMSG_DESTROY_OBJECT = 0x0AA, // ---- Chat ---- diff --git a/include/network/world_socket.hpp b/include/network/world_socket.hpp index c256fbe5..86f869c6 100644 --- a/include/network/world_socket.hpp +++ b/include/network/world_socket.hpp @@ -85,6 +85,10 @@ private: // Receive buffer std::vector receiveBuffer; + // Track how many header bytes have been decrypted (0-4) + // This prevents re-decrypting the same header when waiting for more data + size_t headerBytesDecrypted = 0; + // Packet callback std::function packetCallback; }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 6c07327c..0b55035e 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -621,6 +621,16 @@ void Application::setupUICallbacks() { loadOnlineWorldTerrain(mapId, x, y, z); }); + // Creature spawn callback (online mode) - spawn creature models + gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { + spawnOnlineCreature(guid, displayId, x, y, z, orientation); + }); + + // Creature despawn callback (online mode) - remove creature models + gameHandler->setCreatureDespawnCallback([this](uint64_t guid) { + despawnOnlineCreature(guid); + }); + // "Create Character" button on character screen uiManager->getCharacterScreen().setOnCreateCharacter([this]() { uiManager->getCharacterCreateScreen().reset(); @@ -1567,5 +1577,143 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float setState(AppState::IN_GAME); } +void Application::buildCreatureDisplayLookups() { + if (creatureLookupsBuilt_ || !assetManager || !assetManager->isInitialized()) return; + + LOG_INFO("Building creature display lookups from DBC files"); + + // CreatureDisplayInfo.dbc: displayId (col 0) → modelId (col 1) + if (auto cdi = assetManager->loadDBC("CreatureDisplayInfo.dbc"); cdi && cdi->isLoaded()) { + for (uint32_t i = 0; i < cdi->getRecordCount(); i++) { + displayToModelId_[cdi->getUInt32(i, 0)] = cdi->getUInt32(i, 1); + } + LOG_INFO("Loaded ", displayToModelId_.size(), " display→model mappings"); + } + + // CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2) + if (auto cmd = assetManager->loadDBC("CreatureModelData.dbc"); cmd && cmd->isLoaded()) { + for (uint32_t i = 0; i < cmd->getRecordCount(); i++) { + std::string mdx = cmd->getString(i, 2); + if (mdx.empty()) continue; + // Convert .mdx to .m2 + if (mdx.size() >= 4) { + mdx = mdx.substr(0, mdx.size() - 4) + ".m2"; + } + modelIdToPath_[cmd->getUInt32(i, 0)] = mdx; + } + LOG_INFO("Loaded ", modelIdToPath_.size(), " model→path mappings"); + } + + creatureLookupsBuilt_ = true; +} + +std::string Application::getModelPathForDisplayId(uint32_t displayId) const { + auto itModel = displayToModelId_.find(displayId); + if (itModel == displayToModelId_.end()) return ""; + + auto itPath = modelIdToPath_.find(itModel->second); + if (itPath == modelIdToPath_.end()) return ""; + + return itPath->second; +} + +void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { + if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return; + + // Build lookups on first creature spawn + if (!creatureLookupsBuilt_) { + buildCreatureDisplayLookups(); + } + + // Skip if already spawned + if (creatureInstances_.count(guid)) return; + + // Get model path from displayId + std::string m2Path = getModelPathForDisplayId(displayId); + if (m2Path.empty()) { + LOG_WARNING("No model path for displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")"); + return; + } + + auto* charRenderer = renderer->getCharacterRenderer(); + + // Load model if not already loaded for this displayId + uint32_t modelId = nextCreatureModelId_++; + + auto m2Data = assetManager->readFile(m2Path); + if (m2Data.empty()) { + LOG_WARNING("Failed to read creature M2: ", m2Path); + return; + } + + pipeline::M2Model model = pipeline::M2Loader::load(m2Data); + if (model.vertices.empty()) { + LOG_WARNING("Failed to parse creature M2: ", m2Path); + return; + } + + // Load skin file + std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; + auto skinData = assetManager->readFile(skinPath); + if (!skinData.empty()) { + pipeline::M2Loader::loadSkin(skinData, model); + } + + // Load external .anim files for sequences without flag 0x20 + std::string basePath = m2Path.substr(0, m2Path.size() - 3); + for (uint32_t si = 0; si < model.sequences.size(); si++) { + if (!(model.sequences[si].flags & 0x20)) { + 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); + if (!animData.empty()) { + pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model); + } + } + } + + if (!charRenderer->loadModel(model, modelId)) { + LOG_WARNING("Failed to load creature model: ", m2Path); + return; + } + + // Convert canonical → render coordinates + glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); + + // Create instance + uint32_t instanceId = charRenderer->createInstance(modelId, renderPos, + glm::vec3(0.0f, 0.0f, orientation), 1.0f); + + if (instanceId == 0) { + LOG_WARNING("Failed to create creature instance for guid 0x", std::hex, guid, std::dec); + return; + } + + // Play idle animation + charRenderer->playAnimation(instanceId, 0, true); + + // Track instance + creatureInstances_[guid] = instanceId; + creatureModelIds_[guid] = modelId; + + LOG_INFO("Spawned creature: guid=0x", std::hex, guid, std::dec, + " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); +} + +void Application::despawnOnlineCreature(uint64_t guid) { + auto it = creatureInstances_.find(guid); + if (it == creatureInstances_.end()) return; + + if (renderer && renderer->getCharacterRenderer()) { + renderer->getCharacterRenderer()->removeInstance(it->second); + } + + creatureInstances_.erase(it); + creatureModelIds_.erase(guid); + + LOG_INFO("Despawned creature: guid=0x", std::hex, guid, std::dec); +} + } // namespace core } // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index df43d74a..eefbbec9 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace wowee { namespace game { @@ -925,12 +926,21 @@ void GameHandler::handlePacket(network::Packet& packet) { break; case Opcode::SMSG_UPDATE_OBJECT: + LOG_INFO("Received SMSG_UPDATE_OBJECT, state=", static_cast(state), " size=", packet.getSize()); // Can be received after entering world if (state == WorldState::IN_WORLD) { handleUpdateObject(packet); } break; + case Opcode::SMSG_COMPRESSED_UPDATE_OBJECT: + LOG_INFO("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast(state), " size=", packet.getSize()); + // Compressed version of UPDATE_OBJECT + if (state == WorldState::IN_WORLD) { + handleCompressedUpdateObject(packet); + } + break; + case Opcode::SMSG_DESTROY_OBJECT: // Can be received after entering world if (state == WorldState::IN_WORLD) { @@ -2288,6 +2298,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { for (uint64_t guid : data.outOfRangeGuids) { if (entityManager.hasEntity(guid)) { LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec); + // Trigger creature despawn callback before removing entity + if (creatureDespawnCallback_) { + creatureDespawnCallback_(guid); + } entityManager.removeEntity(guid); } } @@ -2363,9 +2377,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { case 32: unit->setMaxHealth(val); break; case 33: unit->setMaxPower(val); break; case 54: unit->setLevel(val); break; + case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID default: break; } } + // Trigger creature spawn callback for units with displayId + if (block.objectType == ObjectType::UNIT && unit->getDisplayId() != 0) { + if (creatureSpawnCallback_) { + creatureSpawnCallback_(block.guid, unit->getDisplayId(), + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + } + } } // Extract XP fields for player entity if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { @@ -2444,6 +2466,44 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { LOG_INFO("Entity count: ", entityManager.getEntityCount()); } +void GameHandler::handleCompressedUpdateObject(network::Packet& packet) { + LOG_INFO("Handling SMSG_COMPRESSED_UPDATE_OBJECT, packet size: ", packet.getSize()); + + // First 4 bytes = decompressed size + if (packet.getSize() < 4) { + LOG_WARNING("SMSG_COMPRESSED_UPDATE_OBJECT too small"); + return; + } + + uint32_t decompressedSize = packet.readUInt32(); + LOG_INFO(" Decompressed size: ", decompressedSize); + + if (decompressedSize == 0 || decompressedSize > 1024 * 1024) { + LOG_WARNING("Invalid decompressed size: ", decompressedSize); + return; + } + + // Remaining data is zlib compressed + size_t compressedSize = packet.getSize() - packet.getReadPos(); + const uint8_t* compressedData = packet.getData().data() + packet.getReadPos(); + + // Decompress + std::vector decompressed(decompressedSize); + uLongf destLen = decompressedSize; + int ret = uncompress(decompressed.data(), &destLen, compressedData, compressedSize); + + if (ret != Z_OK) { + LOG_WARNING("Failed to decompress UPDATE_OBJECT: zlib error ", ret); + return; + } + + LOG_DEBUG(" Decompressed ", compressedSize, " -> ", destLen, " bytes"); + + // Create packet from decompressed data and parse it + network::Packet decompressedPacket(static_cast(Opcode::SMSG_UPDATE_OBJECT), decompressed); + handleUpdateObject(decompressedPacket); +} + void GameHandler::handleDestroyObject(network::Packet& packet) { LOG_INFO("Handling SMSG_DESTROY_OBJECT"); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index aaf6061f..b5c4297a 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -589,28 +589,195 @@ uint64_t UpdateObjectParser::readPackedGuid(network::Packet& packet) { } bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - // Skip movement flags and other movement data for now - // This is a simplified implementation + // WoW 3.3.5a UPDATE_OBJECT movement block structure: + // 1. UpdateFlags (1 byte, sometimes 2) + // 2. Movement data depends on update flags - // Read movement flags (not used yet) - /*uint32_t flags =*/ packet.readUInt32(); - /*uint16_t flags2 =*/ packet.readUInt16(); + // Update flags (3.3.5a uses 2 bytes for flags) + uint16_t updateFlags = packet.readUInt16(); - // Read timestamp (not used yet) - /*uint32_t time =*/ packet.readUInt32(); + LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec); - // Read position - block.x = packet.readFloat(); - block.y = packet.readFloat(); - block.z = packet.readFloat(); - block.orientation = packet.readFloat(); + // UpdateFlags bit meanings: + // 0x0001 = UPDATEFLAG_SELF + // 0x0002 = UPDATEFLAG_TRANSPORT + // 0x0004 = UPDATEFLAG_HAS_TARGET + // 0x0008 = UPDATEFLAG_LOWGUID + // 0x0010 = UPDATEFLAG_HIGHGUID + // 0x0020 = UPDATEFLAG_LIVING + // 0x0040 = UPDATEFLAG_STATIONARY_POSITION + // 0x0080 = UPDATEFLAG_VEHICLE + // 0x0100 = UPDATEFLAG_POSITION (transport) + // 0x0200 = UPDATEFLAG_ROTATION - block.hasMovement = true; + const uint16_t UPDATEFLAG_LIVING = 0x0020; + const uint16_t UPDATEFLAG_STATIONARY_POSITION = 0x0040; + const uint16_t UPDATEFLAG_HAS_TARGET = 0x0004; + const uint16_t UPDATEFLAG_TRANSPORT = 0x0002; + const uint16_t UPDATEFLAG_POSITION = 0x0100; + const uint16_t UPDATEFLAG_VEHICLE = 0x0080; + const uint16_t UPDATEFLAG_ROTATION = 0x0200; + const uint16_t UPDATEFLAG_LOWGUID = 0x0008; + const uint16_t UPDATEFLAG_HIGHGUID = 0x0010; - LOG_DEBUG(" Movement: (", block.x, ", ", block.y, ", ", block.z, "), orientation=", block.orientation); + if (updateFlags & UPDATEFLAG_LIVING) { + // Full movement block for living units + uint32_t moveFlags = packet.readUInt32(); + uint16_t moveFlags2 = packet.readUInt16(); + /*uint32_t time =*/ packet.readUInt32(); - // TODO: Parse additional movement fields based on flags - // For now, we'll skip them to keep this simple + // Position + block.x = packet.readFloat(); + block.y = packet.readFloat(); + block.z = packet.readFloat(); + block.orientation = packet.readFloat(); + block.hasMovement = true; + + LOG_DEBUG(" LIVING movement: (", block.x, ", ", block.y, ", ", block.z, + "), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec); + + // Transport data (if on transport) + if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT + /*uint64_t transportGuid =*/ readPackedGuid(packet); + /*float tX =*/ packet.readFloat(); + /*float tY =*/ packet.readFloat(); + /*float tZ =*/ packet.readFloat(); + /*float tO =*/ packet.readFloat(); + /*uint32_t tTime =*/ packet.readUInt32(); + /*int8_t tSeat =*/ packet.readUInt8(); + + if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT + /*uint32_t tTime2 =*/ packet.readUInt32(); + } + } + + // Swimming/flying pitch + if ((moveFlags & 0x02000000) || (moveFlags2 & 0x0010)) { // MOVEMENTFLAG_SWIMMING or MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING + /*float pitch =*/ packet.readFloat(); + } + + // Fall time + /*uint32_t fallTime =*/ packet.readUInt32(); + + // Jumping + if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING + /*float jumpVelocity =*/ packet.readFloat(); + /*float jumpSinAngle =*/ packet.readFloat(); + /*float jumpCosAngle =*/ packet.readFloat(); + /*float jumpXYSpeed =*/ packet.readFloat(); + } + + // Spline elevation + if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION + /*float splineElevation =*/ packet.readFloat(); + } + + // Speeds (7 speed values) + /*float walkSpeed =*/ packet.readFloat(); + /*float runSpeed =*/ packet.readFloat(); + /*float runBackSpeed =*/ packet.readFloat(); + /*float swimSpeed =*/ packet.readFloat(); + /*float swimBackSpeed =*/ packet.readFloat(); + /*float flightSpeed =*/ packet.readFloat(); + /*float flightBackSpeed =*/ packet.readFloat(); + /*float turnRate =*/ packet.readFloat(); + /*float pitchRate =*/ packet.readFloat(); + + // Spline data + if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED + // Skip spline data for now - complex structure + uint32_t splineFlags = packet.readUInt32(); + + if (splineFlags & 0x00010000) { // has final point + /*float finalX =*/ packet.readFloat(); + /*float finalY =*/ packet.readFloat(); + /*float finalZ =*/ packet.readFloat(); + } else if (splineFlags & 0x00020000) { // has final target + /*uint64_t finalTarget =*/ packet.readUInt64(); + } else if (splineFlags & 0x00040000) { // has final angle + /*float finalAngle =*/ packet.readFloat(); + } + + /*uint32_t timePassed =*/ packet.readUInt32(); + /*uint32_t duration =*/ packet.readUInt32(); + /*uint32_t splineId =*/ packet.readUInt32(); + + /*float durationMod =*/ packet.readFloat(); + /*float durationModNext =*/ packet.readFloat(); + + /*float verticalAccel =*/ packet.readFloat(); + + /*uint32_t effectStartTime =*/ packet.readUInt32(); + + uint32_t pointCount = packet.readUInt32(); + for (uint32_t i = 0; i < pointCount; i++) { + /*float px =*/ packet.readFloat(); + /*float py =*/ packet.readFloat(); + /*float pz =*/ packet.readFloat(); + } + + /*uint8_t splineMode =*/ packet.readUInt8(); + /*float endPointX =*/ packet.readFloat(); + /*float endPointY =*/ packet.readFloat(); + /*float endPointZ =*/ packet.readFloat(); + } + } + else if (updateFlags & UPDATEFLAG_POSITION) { + // Transport position update + /*uint64_t transportGuid =*/ readPackedGuid(packet); + block.x = packet.readFloat(); + block.y = packet.readFloat(); + block.z = packet.readFloat(); + /*float transportOffsetX =*/ packet.readFloat(); + /*float transportOffsetY =*/ packet.readFloat(); + /*float transportOffsetZ =*/ packet.readFloat(); + block.orientation = packet.readFloat(); + /*float corpseOrientation =*/ packet.readFloat(); + block.hasMovement = true; + + LOG_DEBUG(" POSITION: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation); + } + else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) { + // Simple stationary position (4 floats) + block.x = packet.readFloat(); + block.y = packet.readFloat(); + block.z = packet.readFloat(); + block.orientation = packet.readFloat(); + block.hasMovement = true; + + LOG_DEBUG(" STATIONARY: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation); + } + + // Target GUID (for units with target) + if (updateFlags & UPDATEFLAG_HAS_TARGET) { + /*uint64_t targetGuid =*/ readPackedGuid(packet); + } + + // Transport time + if (updateFlags & UPDATEFLAG_TRANSPORT) { + /*uint32_t transportTime =*/ packet.readUInt32(); + } + + // Vehicle + if (updateFlags & UPDATEFLAG_VEHICLE) { + /*uint32_t vehicleId =*/ packet.readUInt32(); + /*float vehicleOrientation =*/ packet.readFloat(); + } + + // Rotation (GameObjects) + if (updateFlags & UPDATEFLAG_ROTATION) { + /*int64_t rotation =*/ packet.readUInt64(); + } + + // Low GUID + if (updateFlags & UPDATEFLAG_LOWGUID) { + /*uint32_t lowGuid =*/ packet.readUInt32(); + } + + // High GUID + if (updateFlags & UPDATEFLAG_HIGHGUID) { + /*uint32_t highGuid =*/ packet.readUInt32(); + } return true; } diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 95cddcf4..9d798973 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -79,6 +79,7 @@ void WorldSocket::disconnect() { connected = false; encryptionEnabled = false; receiveBuffer.clear(); + headerBytesDecrypted = 0; LOG_INFO("Disconnected from world server"); } @@ -197,31 +198,30 @@ void WorldSocket::update() { void WorldSocket::tryParsePackets() { // World server packets have 4-byte incoming header: size(2) + opcode(2) while (receiveBuffer.size() >= 4) { - // Copy header for decryption - uint8_t header[4]; - memcpy(header, receiveBuffer.data(), 4); - - // Decrypt header if encryption is enabled - if (encryptionEnabled) { - decryptCipher.process(header, 4); + // Decrypt header bytes in-place if encryption is enabled + // Only decrypt bytes we haven't already decrypted + if (encryptionEnabled && headerBytesDecrypted < 4) { + size_t toDecrypt = 4 - headerBytesDecrypted; + decryptCipher.process(receiveBuffer.data() + headerBytesDecrypted, toDecrypt); + headerBytesDecrypted = 4; } - // Parse header + // Parse header (now decrypted in-place) // Size: 2 bytes big-endian (includes opcode, so payload = size - 2) - uint16_t size = (header[0] << 8) | header[1]; + uint16_t size = (receiveBuffer[0] << 8) | receiveBuffer[1]; // Opcode: 2 bytes little-endian - uint16_t opcode = header[2] | (header[3] << 8); + uint16_t opcode = receiveBuffer[2] | (receiveBuffer[3] << 8); LOG_DEBUG("RECV encryptionEnabled=", encryptionEnabled, - " header=[", std::hex, (int)header[0], " ", (int)header[1], " ", - (int)header[2], " ", (int)header[3], std::dec, "]", + " header=[", std::hex, (int)receiveBuffer[0], " ", (int)receiveBuffer[1], " ", + (int)receiveBuffer[2], " ", (int)receiveBuffer[3], std::dec, "]", " -> size=", size, " opcode=0x", std::hex, opcode, std::dec); // Total packet size: size field (2) + size value (which includes opcode + payload) size_t totalSize = 2 + size; if (receiveBuffer.size() < totalSize) { - // Not enough data yet + // Not enough data yet - header stays decrypted in buffer LOG_DEBUG("Waiting for more data: have ", receiveBuffer.size(), " bytes, need ", totalSize); break; @@ -249,8 +249,9 @@ void WorldSocket::tryParsePackets() { // Create packet with opcode and payload Packet packet(opcode, packetData); - // Remove parsed data from buffer + // Remove parsed data from buffer and reset header decryption counter receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize); + headerBytesDecrypted = 0; // Call callback if set if (packetCallback) {