diff --git a/Data/expansions/turtle/expansion.json b/Data/expansions/turtle/expansion.json index b7ac306e..5341a119 100644 --- a/Data/expansions/turtle/expansion.json +++ b/Data/expansions/turtle/expansion.json @@ -6,7 +6,7 @@ "build": 7234, "worldBuild": 5875, "protocolVersion": 3, - "os": "OSX", + "os": "Win", "locale": "enGB", "maxLevel": 60, "races": [1, 2, 3, 4, 5, 6, 7, 8], diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 6a3ab5a4..f2d43d1e 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -861,6 +861,9 @@ private: void handleMonsterMove(network::Packet& packet); void handleMonsterMoveTransport(network::Packet& packet); + // ---- Other player movement (MSG_MOVE_* from server) ---- + void handleOtherPlayerMovement(network::Packet& packet); + // ---- Phase 5 handlers ---- void handleLootResponse(network::Packet& packet); void handleLootReleaseResponse(network::Packet& packet); @@ -1226,6 +1229,19 @@ private: std::vector wardenModuleKey_; // 16 bytes RC4 uint32_t wardenModuleSize_ = 0; std::vector wardenModuleData_; // Downloaded module chunks + std::vector wardenLoadedModuleImage_; // Parsed module image for key derivation + + // Pre-computed challenge/response entries from .cr file + struct WardenCREntry { + uint8_t seed[16]; + uint8_t reply[20]; + uint8_t clientKey[16]; // Encrypt key (client→server) + uint8_t serverKey[16]; // Decrypt key (server→client) + }; + std::vector wardenCREntries_; + // Module-specific check type opcodes [9]: MEM, PAGE_A, PAGE_B, MPQ, LUA, DRIVER, TIMING, PROC, MODULE + uint8_t wardenCheckOpcodes_[9] = {}; + bool loadWardenCRFile(const std::string& moduleHashHex); // ---- XP tracking ---- uint32_t playerXp_ = 0; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 1fef7cdd..0f58f109 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -91,6 +91,20 @@ public: return AuraUpdateParser::parse(packet, data, isAll); } + // --- Chat --- + + /** Parse SMSG_MESSAGECHAT */ + virtual bool parseMessageChat(network::Packet& packet, MessageChatData& data) { + return MessageChatParser::parse(packet, data); + } + + // --- Destroy Object --- + + /** Parse SMSG_DESTROY_OBJECT */ + virtual bool parseDestroyObject(network::Packet& packet, DestroyObjectData& data) { + return DestroyObjectParser::parse(packet, data); + } + // --- Utility --- /** Read a packed GUID from the packet */ @@ -163,6 +177,7 @@ public: network::Packet buildMovementPacket(LogicalOpcode opcode, const MovementInfo& info, uint64_t playerGuid = 0) override; + bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; }; /** diff --git a/src/core/application.cpp b/src/core/application.cpp index 4726c4e7..5944facb 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2386,7 +2386,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) { uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0); std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1); - if (!internalName.empty()) { + if (!internalName.empty() && mapNameById.find(id) == mapNameById.end()) { mapNameById[id] = std::move(internalName); } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b2e0e4f9..0ea3e031 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -28,7 +29,7 @@ #include #include #include -#include +#include #include namespace wowee { @@ -1382,6 +1383,27 @@ void GameHandler::handlePacket(network::Packet& packet) { LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS"); break; + // ---- MSG_MOVE_* opcodes (server relays other players' movement) ---- + case Opcode::CMSG_MOVE_START_FORWARD: + case Opcode::CMSG_MOVE_START_BACKWARD: + case Opcode::CMSG_MOVE_STOP: + case Opcode::CMSG_MOVE_START_STRAFE_LEFT: + case Opcode::CMSG_MOVE_START_STRAFE_RIGHT: + case Opcode::CMSG_MOVE_STOP_STRAFE: + case Opcode::CMSG_MOVE_JUMP: + case Opcode::CMSG_MOVE_START_TURN_LEFT: + case Opcode::CMSG_MOVE_START_TURN_RIGHT: + case Opcode::CMSG_MOVE_STOP_TURN: + case Opcode::CMSG_MOVE_SET_FACING: + case Opcode::CMSG_MOVE_FALL_LAND: + case Opcode::CMSG_MOVE_HEARTBEAT: + case Opcode::CMSG_MOVE_START_SWIM: + case Opcode::CMSG_MOVE_STOP_SWIM: + if (state == WorldState::IN_WORLD) { + handleOtherPlayerMovement(packet); + } + break; + default: // In pre-world states we need full visibility (char create/login handshakes). // In-world we keep de-duplication to avoid heavy log I/O in busy areas. @@ -1907,6 +1929,67 @@ void GameHandler::handleTutorialFlags(network::Packet& packet) { flags[4], ", ", flags[5], ", ", flags[6], ", ", flags[7], "]"); } +bool GameHandler::loadWardenCRFile(const std::string& moduleHashHex) { + wardenCREntries_.clear(); + + // Look for .cr file in warden cache + std::string homeDir; + if (const char* h = std::getenv("HOME")) homeDir = h; + else homeDir = "."; + + std::string crPath = homeDir + "/.local/share/wowee/warden_cache/" + moduleHashHex + ".cr"; + + std::ifstream crFile(crPath, std::ios::binary); + if (!crFile) { + LOG_WARNING("Warden: No .cr file found at ", crPath); + return false; + } + + // Get file size + crFile.seekg(0, std::ios::end); + auto fileSize = crFile.tellg(); + crFile.seekg(0, std::ios::beg); + + // Header: [4 memoryRead][4 pageScanCheck][9 opcodes] = 17 bytes + constexpr size_t CR_HEADER_SIZE = 17; + constexpr size_t CR_ENTRY_SIZE = 68; // seed[16]+reply[20]+clientKey[16]+serverKey[16] + + if (static_cast(fileSize) < CR_HEADER_SIZE) { + LOG_ERROR("Warden: .cr file too small (", fileSize, " bytes)"); + return false; + } + + // Read header: [4 memoryRead][4 pageScanCheck][9 opcodes] + crFile.seekg(8); // skip memoryRead + pageScanCheck + crFile.read(reinterpret_cast(wardenCheckOpcodes_), 9); + { + std::string opcHex; + const char* names[] = {"MEM","PAGE_A","PAGE_B","MPQ","LUA","DRIVER","TIMING","PROC","MODULE"}; + for (int i = 0; i < 9; i++) { + char s[16]; snprintf(s, sizeof(s), "%s=0x%02X ", names[i], wardenCheckOpcodes_[i]); opcHex += s; + } + LOG_INFO("Warden: Check opcodes: ", opcHex); + } + + size_t entryCount = (static_cast(fileSize) - CR_HEADER_SIZE) / CR_ENTRY_SIZE; + if (entryCount == 0) { + LOG_ERROR("Warden: .cr file has no entries"); + return false; + } + + wardenCREntries_.resize(entryCount); + for (size_t i = 0; i < entryCount; i++) { + auto& e = wardenCREntries_[i]; + crFile.read(reinterpret_cast(e.seed), 16); + crFile.read(reinterpret_cast(e.reply), 20); + crFile.read(reinterpret_cast(e.clientKey), 16); + crFile.read(reinterpret_cast(e.serverKey), 16); + } + + LOG_INFO("Warden: Loaded ", entryCount, " CR entries from ", crPath); + return true; +} + void GameHandler::handleWardenData(network::Packet& packet) { const auto& data = packet.getData(); if (!wardenGateSeen_) { @@ -1938,7 +2021,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { // Log decrypted data { std::string hex; - size_t logSize = std::min(decrypted.size(), size_t(64)); + size_t logSize = std::min(decrypted.size(), size_t(256)); hex.reserve(logSize * 3); for (size_t i = 0; i < logSize; ++i) { char b[4]; snprintf(b, sizeof(b), "%02x ", decrypted[i]); hex += b; @@ -1990,6 +2073,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { for (auto b : wardenModuleKey_) { char s[4]; snprintf(s, 4, "%02x", b); keyHex += s; } LOG_INFO("Warden: MODULE_USE hash=", hashHex, " key=", keyHex, " size=", wardenModuleSize_); + + // Try to load pre-computed challenge/response entries + loadWardenCRFile(hashHex); } // Respond with MODULE_MISSING (opcode 0x00) to request the module data @@ -2052,64 +2138,66 @@ void GameHandler::handleWardenData(network::Packet& packet) { LOG_INFO("Warden: HASH_REQUEST seed=", seedHex); } - // The server expects SHA1 of the module's init function stub XOR'd with the seed. - // Without the actual module execution, we compute a plausible hash. - // For now, try to use the module data we downloaded. - // The correct approach: decrypt module with moduleKey RC4, then hash the init stub. - // But we need to at least send a HASH_RESULT to not get kicked immediately. - - // Decrypt the downloaded module data using the module RC4 key - // VMaNGOS: The module is RC4-encrypted with wardenModuleKey_ - if (!wardenModuleData_.empty() && !wardenModuleKey_.empty()) { - LOG_INFO("Warden: Attempting to compute hash from downloaded module (", - wardenModuleData_.size(), " bytes)"); - - // The module data is RC4-encrypted. Decrypt it. - // Standard RC4 decryption with the module key - std::vector moduleDecrypted(wardenModuleData_.size()); - { - // RC4 KSA + PRGA with wardenModuleKey_ - std::vector S(256); - for (int i = 0; i < 256; ++i) S[i] = static_cast(i); - uint8_t j = 0; - for (int i = 0; i < 256; ++i) { - j = (j + S[i] + wardenModuleKey_[i % wardenModuleKey_.size()]) & 0xFF; - std::swap(S[i], S[j]); - } - uint8_t ri = 0, rj = 0; - for (size_t k = 0; k < wardenModuleData_.size(); ++k) { - ri = (ri + 1) & 0xFF; - rj = (rj + S[ri]) & 0xFF; - std::swap(S[ri], S[rj]); - moduleDecrypted[k] = wardenModuleData_[k] ^ S[(S[ri] + S[rj]) & 0xFF]; + // --- Try CR lookup (pre-computed challenge/response entries) --- + if (!wardenCREntries_.empty()) { + const WardenCREntry* match = nullptr; + for (const auto& entry : wardenCREntries_) { + if (std::memcmp(entry.seed, seed.data(), 16) == 0) { + match = &entry; + break; } } - LOG_INFO("Warden: Module decrypted, computing hash response"); + if (match) { + LOG_INFO("Warden: Found matching CR entry for seed"); - // The hash is SHA1 of the seed concatenated with module data - // Actually in VMaNGOS, the init function stub computes SHA1 of seed + specific module regions. - // We'll try a simple SHA1(seed + first 16 bytes of decrypted module) approach, - // which won't pass verification but at least sends a properly formatted response. - std::vector hashInput; - hashInput.insert(hashInput.end(), seed.begin(), seed.end()); - // Add decrypted module data - hashInput.insert(hashInput.end(), moduleDecrypted.begin(), moduleDecrypted.end()); - auto hash = auth::Crypto::sha1(hashInput); + // Log the reply we're sending + { + std::string replyHex; + for (int i = 0; i < 20; i++) { + char s[4]; snprintf(s, 4, "%02x", match->reply[i]); replyHex += s; + } + LOG_INFO("Warden: Sending pre-computed reply=", replyHex); + } - // Send HASH_RESULT (WARDEN_CMSG_HASH_RESULT = opcode 0x04) - std::vector resp; - resp.push_back(0x04); // WARDEN_CMSG_HASH_RESULT - resp.insert(resp.end(), hash.begin(), hash.end()); // 20-byte SHA1 - sendWardenResponse(resp); - LOG_INFO("Warden: Sent HASH_RESULT (21 bytes)"); - } else { - // No module data available, send a dummy hash - LOG_WARNING("Warden: No module data for hash computation, sending dummy"); - std::vector hashInput; - hashInput.insert(hashInput.end(), seed.begin(), seed.end()); - auto hash = auth::Crypto::sha1(hashInput); + // Send HASH_RESULT (opcode 0x04 + 20-byte reply) + std::vector resp; + resp.push_back(0x04); + resp.insert(resp.end(), match->reply, match->reply + 20); + sendWardenResponse(resp); + // Switch to new RC4 keys from the CR entry + // clientKey = encrypt (client→server), serverKey = decrypt (server→client) + std::vector newEncryptKey(match->clientKey, match->clientKey + 16); + std::vector newDecryptKey(match->serverKey, match->serverKey + 16); + wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey); + + { + std::string ekHex, dkHex; + for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newEncryptKey[i]); ekHex += s; } + for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newDecryptKey[i]); dkHex += s; } + LOG_INFO("Warden: Switched to CR keys encrypt=", ekHex, " decrypt=", dkHex); + } + + wardenState_ = WardenState::WAIT_CHECKS; + break; + } else { + LOG_WARNING("Warden: Seed not found in ", wardenCREntries_.size(), " CR entries"); + } + } + + // --- Fallback: SHA1(seed + moduleImage) if no CR match --- + LOG_WARNING("Warden: No CR match, falling back to SHA1 hash computation"); + + if (wardenModuleData_.empty() || wardenModuleKey_.empty()) { + LOG_ERROR("Warden: No module data and no CR match — cannot compute hash"); + wardenState_ = WardenState::WAIT_CHECKS; + break; + } + + // SHA1 fallback (unlikely to work for vanilla modules, but log for debugging) + { + auto hash = auth::Crypto::sha1(seed); std::vector resp; resp.push_back(0x04); resp.insert(resp.end(), hash.begin(), hash.end()); @@ -2122,19 +2210,183 @@ void GameHandler::handleWardenData(network::Packet& packet) { case 0x02: { // WARDEN_SMSG_CHEAT_CHECKS_REQUEST LOG_INFO("Warden: CHEAT_CHECKS_REQUEST (", decrypted.size(), " bytes)"); - // We don't have the module to properly process checks. - // Send a minimal valid-looking response. - // WARDEN_CMSG_CHEAT_CHECKS_RESULT (opcode 0x02): - // [1 opcode][2 length LE][4 checksum][result data] - // Minimal response: length=0, checksum=0 + if (decrypted.size() < 3) { + LOG_ERROR("Warden: CHEAT_CHECKS_REQUEST too short"); + break; + } + + // --- Parse string table --- + // Format: [1 opcode][string table: (len+data)*][0x00 end][check data][xorByte] + size_t pos = 1; + std::vector strings; + while (pos < decrypted.size()) { + uint8_t slen = decrypted[pos++]; + if (slen == 0) break; // end of string table + if (pos + slen > decrypted.size()) break; + strings.emplace_back(reinterpret_cast(decrypted.data() + pos), slen); + pos += slen; + } + LOG_INFO("Warden: String table: ", strings.size(), " entries"); + for (size_t i = 0; i < strings.size(); i++) { + LOG_INFO("Warden: [", i, "] = \"", strings[i], "\""); + } + + // XOR byte is the last byte of the packet + uint8_t xorByte = decrypted.back(); + LOG_INFO("Warden: XOR byte = 0x", [&]{ char s[4]; snprintf(s,4,"%02x",xorByte); return std::string(s); }()); + + // Check type enum indices + enum CheckType { CT_MEM=0, CT_PAGE_A=1, CT_PAGE_B=2, CT_MPQ=3, CT_LUA=4, + CT_DRIVER=5, CT_TIMING=6, CT_PROC=7, CT_MODULE=8, CT_UNKNOWN=9 }; + const char* checkTypeNames[] = {"MEM","PAGE_A","PAGE_B","MPQ","LUA","DRIVER","TIMING","PROC","MODULE","UNKNOWN"}; + + auto decodeCheckType = [&](uint8_t raw) -> CheckType { + uint8_t decoded = raw ^ xorByte; + for (int i = 0; i < 9; i++) { + if (decoded == wardenCheckOpcodes_[i]) return static_cast(i); + } + return CT_UNKNOWN; + }; + + // --- Parse check entries and build response --- + std::vector resultData; + size_t checkEnd = decrypted.size() - 1; // exclude xorByte + int checkCount = 0; + + while (pos < checkEnd) { + CheckType ct = decodeCheckType(decrypted[pos]); + pos++; + checkCount++; + + LOG_INFO("Warden: Check #", checkCount, " type=", checkTypeNames[ct], + " at offset ", pos - 1); + + switch (ct) { + case CT_TIMING: { + // No additional request data + // Response: [uint8 result=1][uint32 ticks] + resultData.push_back(0x01); + uint32_t ticks = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + resultData.push_back(ticks & 0xFF); + resultData.push_back((ticks >> 8) & 0xFF); + resultData.push_back((ticks >> 16) & 0xFF); + resultData.push_back((ticks >> 24) & 0xFF); + break; + } + case CT_MEM: { + // Request: [1 stringIdx][4 offset][1 length] + if (pos + 6 > checkEnd) { pos = checkEnd; break; } + pos++; // stringIdx + uint32_t offset = decrypted[pos] | (uint32_t(decrypted[pos+1])<<8) + | (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24); + pos += 4; + uint8_t readLen = decrypted[pos++]; + LOG_INFO("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), + " len=", (int)readLen); + // Response: [uint8 result=0][data zeros] + // We don't have real memory, send zeros + resultData.push_back(0x00); + for (int i = 0; i < readLen; i++) resultData.push_back(0x00); + break; + } + case CT_PAGE_A: + case CT_PAGE_B: { + // Request: [4 seed][20 sha1][4 addr][1 length] + if (pos + 29 > checkEnd) { pos = checkEnd; break; } + pos += 29; + // Response: [uint8 result=0] (page matches expected) + resultData.push_back(0x00); + break; + } + case CT_MPQ: { + // Request: [1 stringIdx] + if (pos + 1 > checkEnd) { pos = checkEnd; break; } + uint8_t strIdx = decrypted[pos++]; + LOG_INFO("Warden: MPQ file=\"", + (strIdx < strings.size() ? strings[strIdx] : "?"), "\""); + // Response: [uint8 result=0][20 sha1 zeros] + // Pretend file found with zero hash (may fail comparison) + resultData.push_back(0x00); + for (int i = 0; i < 20; i++) resultData.push_back(0x00); + break; + } + case CT_LUA: { + // Request: [1 stringIdx] + if (pos + 1 > checkEnd) { pos = checkEnd; break; } + uint8_t strIdx = decrypted[pos++]; + LOG_INFO("Warden: LUA str=\"", + (strIdx < strings.size() ? strings[strIdx] : "?"), "\""); + // Response: [uint8 result=0][uint16 len=0] + // Lua string doesn't exist + resultData.push_back(0x01); // not found + break; + } + case CT_DRIVER: { + // Request: [4 seed][20 sha1][1 stringIdx] + if (pos + 25 > checkEnd) { pos = checkEnd; break; } + pos += 24; // skip seed + sha1 + uint8_t strIdx = decrypted[pos++]; + LOG_INFO("Warden: DRIVER=\"", + (strIdx < strings.size() ? strings[strIdx] : "?"), "\""); + // Response: [uint8 result=1] (driver NOT found = clean) + resultData.push_back(0x01); + break; + } + case CT_MODULE: { + // Request: [4 seed][20 sha1] + if (pos + 24 > checkEnd) { pos = checkEnd; break; } + pos += 24; + // Response: [uint8 result=1] (module NOT loaded = clean) + resultData.push_back(0x01); + break; + } + case CT_PROC: { + // Request: [4 seed][20 sha1][1 stringIdx][1 stringIdx2][4 offset] + if (pos + 30 > checkEnd) { pos = checkEnd; break; } + pos += 30; + // Response: [uint8 result=1] (proc NOT found = clean) + resultData.push_back(0x01); + break; + } + default: { + LOG_WARNING("Warden: Unknown check type, cannot parse remaining"); + pos = checkEnd; // stop parsing + break; + } + } + } + + LOG_INFO("Warden: Parsed ", checkCount, " checks, result data size=", resultData.size()); + + // --- Compute checksum: XOR of 5 uint32s from SHA1(resultData) --- + auto resultHash = auth::Crypto::sha1(resultData); + uint32_t checksum = 0; + for (int i = 0; i < 5; i++) { + uint32_t word = resultHash[i*4] + | (uint32_t(resultHash[i*4+1]) << 8) + | (uint32_t(resultHash[i*4+2]) << 16) + | (uint32_t(resultHash[i*4+3]) << 24); + checksum ^= word; + } + + // --- Build response: [0x02][uint16 length][uint32 checksum][resultData] --- + uint16_t resultLen = static_cast(resultData.size()); std::vector resp; - resp.push_back(0x02); // WARDEN_CMSG_CHEAT_CHECKS_RESULT - resp.push_back(0x00); resp.push_back(0x00); // length = 0 - resp.push_back(0x00); resp.push_back(0x00); - resp.push_back(0x00); resp.push_back(0x00); // checksum = 0 + resp.push_back(0x02); + resp.push_back(resultLen & 0xFF); + resp.push_back((resultLen >> 8) & 0xFF); + resp.push_back(checksum & 0xFF); + resp.push_back((checksum >> 8) & 0xFF); + resp.push_back((checksum >> 16) & 0xFF); + resp.push_back((checksum >> 24) & 0xFF); + resp.insert(resp.end(), resultData.begin(), resultData.end()); sendWardenResponse(resp); - LOG_INFO("Warden: Sent CHEAT_CHECKS_RESULT (minimal)"); + LOG_INFO("Warden: Sent CHEAT_CHECKS_RESULT (", resp.size(), " bytes, ", + checkCount, " checks, checksum=0x", + [&]{char s[12];snprintf(s,12,"%08x",checksum);return std::string(s);}(), ")"); break; } @@ -2431,7 +2683,7 @@ void GameHandler::setOrientation(float orientation) { void GameHandler::handleUpdateObject(network::Packet& packet) { UpdateObjectData data; - if (!UpdateObjectParser::parse(packet, data)) { + if (!packetParsers_->parseUpdateObject(packet, data)) { LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT"); return; } @@ -2651,14 +2903,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (unit->getFactionTemplate() != 0) { unit->setHostile(isHostileFaction(unit->getFactionTemplate())); } - // Trigger creature spawn callback for units with displayId - if (block.objectType == ObjectType::UNIT && unit->getDisplayId() != 0) { + // Trigger creature spawn callback for units/players with displayId + if ((block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) && unit->getDisplayId() != 0) { if (creatureSpawnCallback_) { creatureSpawnCallback_(block.guid, unit->getDisplayId(), unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); } // Query quest giver status for NPCs with questgiver flag (0x02) - if ((unit->getNpcFlags() & 0x02) && socket) { + if (block.objectType == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); qsPkt.writeUInt64(block.guid); socket->send(qsPkt); @@ -2981,8 +3233,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } } - // Some units are created without displayId and get it later via VALUES. - if (entity->getType() == ObjectType::UNIT && + // Some units/players are created without displayId and get it later via VALUES. + if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && displayIdChanged && unit->getDisplayId() != 0 && unit->getDisplayId() != oldDisplayId) { @@ -2990,7 +3242,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { creatureSpawnCallback_(block.guid, unit->getDisplayId(), unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); } - if ((unit->getNpcFlags() & 0x02) && socket) { + if (entity->getType() == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); qsPkt.writeUInt64(block.guid); socket->send(qsPkt); @@ -3349,7 +3601,7 @@ void GameHandler::handleMessageChat(network::Packet& packet) { LOG_DEBUG("Handling SMSG_MESSAGECHAT"); MessageChatData data; - if (!MessageChatParser::parse(packet, data)) { + if (!packetParsers_->parseMessageChat(packet, data)) { LOG_WARNING("Failed to parse SMSG_MESSAGECHAT"); return; } @@ -3363,6 +3615,31 @@ void GameHandler::handleMessageChat(network::Packet& packet) { return; } + // Resolve sender name from entity/cache if not already set by parser + if (data.senderName.empty() && data.senderGuid != 0) { + // Check player name cache first + auto nameIt = playerNameCache.find(data.senderGuid); + if (nameIt != playerNameCache.end()) { + data.senderName = nameIt->second; + } else { + // Try entity name + auto entity = entityManager.getEntity(data.senderGuid); + if (entity) { + if (entity->getType() == ObjectType::PLAYER) { + auto player = std::dynamic_pointer_cast(entity); + if (player && !player->getName().empty()) { + data.senderName = player->getName(); + } + } else if (entity->getType() == ObjectType::UNIT) { + auto unit = std::dynamic_pointer_cast(entity); + if (unit && !unit->getName().empty()) { + data.senderName = unit->getName(); + } + } + } + } + } + // Add to chat history chatHistory.push_back(data); @@ -3381,18 +3658,7 @@ void GameHandler::handleMessageChat(network::Packet& packet) { if (!data.senderName.empty()) { senderInfo = data.senderName; } else if (data.senderGuid != 0) { - // Try to find entity name - auto entity = entityManager.getEntity(data.senderGuid); - if (entity && entity->getType() == ObjectType::PLAYER) { - auto player = std::dynamic_pointer_cast(entity); - if (player && !player->getName().empty()) { - senderInfo = player->getName(); - } else { - senderInfo = "Player-" + std::to_string(data.senderGuid); - } - } else { - senderInfo = "Unknown-" + std::to_string(data.senderGuid); - } + senderInfo = "Unknown-" + std::to_string(data.senderGuid); } else { senderInfo = "System"; } @@ -5055,6 +5321,48 @@ void GameHandler::handleArenaError(network::Packet& packet) { LOG_INFO("Arena error: ", error, " - ", msg); } +void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { + // Server relays MSG_MOVE_* for other players: PackedGuid + MovementInfo + uint64_t moverGuid = UpdateObjectParser::readPackedGuid(packet); + if (moverGuid == playerGuid || moverGuid == 0) { + return; // Skip our own echoes + } + + // Read movement info (expansion-specific format) + // For classic: moveFlags(u32) + time(u32) + pos(4xf32) + [transport] + [pitch] + fallTime(u32) + [jump] + [splineElev] + MovementInfo info = {}; + info.flags = packet.readUInt32(); + // WotLK has uint16 flags2, classic/TBC don't + if (build >= 8606) { // TBC+ + if (build >= 12340) { + info.flags2 = packet.readUInt16(); + } else { + info.flags2 = packet.readUInt8(); + } + } + info.time = packet.readUInt32(); + info.x = packet.readFloat(); + info.y = packet.readFloat(); + info.z = packet.readFloat(); + info.orientation = packet.readFloat(); + + // Update entity position in entity manager + auto entity = entityManager.getEntity(moverGuid); + if (!entity) { + return; + } + + // Convert server coords to canonical + glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(info.x, info.y, info.z)); + float canYaw = core::coords::serverToCanonicalYaw(info.orientation); + entity->setPosition(canonical.x, canonical.y, canonical.z, canYaw); + + // Notify renderer + if (creatureMoveCallback_) { + creatureMoveCallback_(moverGuid, canonical.x, canonical.y, canonical.z, 0); + } +} + void GameHandler::handleMonsterMove(network::Packet& packet) { MonsterMoveData data; if (!MonsterMoveParser::parse(packet, data)) { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 3aa8c454..5ae99e6a 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -340,5 +340,109 @@ bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumRespon return true; } +// ============================================================================ +// Classic 1.12.1 parseMessageChat +// Differences from WotLK: +// - NO uint32 unknown field after senderGuid +// - CHANNEL type: channelName + rank(u32) + senderGuid (not just channelName) +// - No ACHIEVEMENT/GUILD_ACHIEVEMENT types +// ============================================================================ +bool ClassicPacketParsers::parseMessageChat(network::Packet& packet, MessageChatData& data) { + if (packet.getSize() < 10) { + LOG_ERROR("[Classic] SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes"); + return false; + } + + // Read chat type + uint8_t typeVal = packet.readUInt8(); + data.type = static_cast(typeVal); + + // Read language + uint32_t langVal = packet.readUInt32(); + data.language = static_cast(langVal); + + // Classic: NO uint32 unknown field here (WotLK has one) + + // Type-specific data + switch (data.type) { + case ChatType::MONSTER_SAY: + case ChatType::MONSTER_YELL: + case ChatType::MONSTER_EMOTE: { + // nameLen(u32) + name + targetGuid(u64) + uint32_t nameLen = packet.readUInt32(); + if (nameLen > 0 && nameLen < 256) { + data.senderName.resize(nameLen); + for (uint32_t i = 0; i < nameLen; ++i) { + data.senderName[i] = static_cast(packet.readUInt8()); + } + // Remove null terminator if present + if (!data.senderName.empty() && data.senderName.back() == '\0') { + data.senderName.pop_back(); + } + } + data.receiverGuid = packet.readUInt64(); + break; + } + + case ChatType::CHANNEL: { + // channelName(string) + rank(u32) + senderGuid(u64) + data.channelName = packet.readString(); + /*uint32_t rank =*/ packet.readUInt32(); + data.senderGuid = packet.readUInt64(); + break; + } + + default: { + // Most types: senderGuid(u64) + data.senderGuid = packet.readUInt64(); + break; + } + } + + // Vanilla/Classic: target GUID follows type-specific header for non-monster types + // (Monster types already read target/receiver in their switch case) + if (data.type != ChatType::MONSTER_SAY && + data.type != ChatType::MONSTER_YELL && + data.type != ChatType::MONSTER_EMOTE && + data.type != ChatType::CHANNEL) { + if (packet.getReadPos() + 12 <= packet.getSize()) { // 8 (guid) + 4 (msgLen) minimum + data.receiverGuid = packet.readUInt64(); + } + } + + // Read message length + uint32_t messageLen = packet.readUInt32(); + + // Read message + if (messageLen > 0 && messageLen < 8192) { + data.message.resize(messageLen); + for (uint32_t i = 0; i < messageLen; ++i) { + data.message[i] = static_cast(packet.readUInt8()); + } + // Remove null terminator if present + if (!data.message.empty() && data.message.back() == '\0') { + data.message.pop_back(); + } + } + + // Read chat tag + if (packet.getReadPos() < packet.getSize()) { + data.chatTag = packet.readUInt8(); + } + + LOG_INFO("[Classic] Parsed SMSG_MESSAGECHAT:"); + LOG_INFO(" Type: ", getChatTypeString(data.type)); + LOG_INFO(" Sender GUID: 0x", std::hex, data.senderGuid, std::dec); + if (!data.senderName.empty()) { + LOG_INFO(" Sender name: ", data.senderName); + } + if (!data.channelName.empty()) { + LOG_INFO(" Channel: ", data.channelName); + } + LOG_INFO(" Message: ", data.message); + + return true; +} + } // namespace game } // namespace wowee diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 1d881806..0444f2b8 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -395,12 +395,51 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa } } - // Parse update blocks + // Parse update blocks — dispatching movement via virtual parseMovementBlock() data.blocks.reserve(data.blockCount); for (uint32_t i = 0; i < data.blockCount; ++i) { LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount); UpdateBlock block; - if (!UpdateObjectParser::parseUpdateBlock(packet, block)) { + + // Read update type + uint8_t updateTypeVal = packet.readUInt8(); + block.updateType = static_cast(updateTypeVal); + LOG_DEBUG("Update block: type=", (int)updateTypeVal); + + bool ok = false; + switch (block.updateType) { + case UpdateType::VALUES: { + block.guid = UpdateObjectParser::readPackedGuid(packet); + ok = UpdateObjectParser::parseUpdateFields(packet, block); + break; + } + case UpdateType::MOVEMENT: { + block.guid = UpdateObjectParser::readPackedGuid(packet); + ok = this->parseMovementBlock(packet, block); + break; + } + case UpdateType::CREATE_OBJECT: + case UpdateType::CREATE_OBJECT2: { + block.guid = UpdateObjectParser::readPackedGuid(packet); + uint8_t objectTypeVal = packet.readUInt8(); + block.objectType = static_cast(objectTypeVal); + ok = this->parseMovementBlock(packet, block); + if (ok) { + ok = UpdateObjectParser::parseUpdateFields(packet, block); + } + break; + } + case UpdateType::OUT_OF_RANGE_OBJECTS: + case UpdateType::NEAR_OBJECTS: + ok = true; + break; + default: + LOG_WARNING("Unknown update type: ", (int)updateTypeVal); + ok = false; + break; + } + + if (!ok) { LOG_ERROR("Failed to parse update block ", i + 1); return false; } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 17c63fb5..5d13485f 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1101,15 +1101,20 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) { // SMSG_DESTROY_OBJECT format: // uint64 guid - // uint8 isDeath (0 = despawn, 1 = death) + // uint8 isDeath (0 = despawn, 1 = death) — WotLK only; vanilla/TBC omit this - if (packet.getSize() < 9) { + if (packet.getSize() < 8) { LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes"); return false; } data.guid = packet.readUInt64(); - data.isDeath = (packet.readUInt8() != 0); + // WotLK adds isDeath byte; vanilla/TBC packets are exactly 8 bytes + if (packet.getReadPos() < packet.getSize()) { + data.isDeath = (packet.readUInt8() != 0); + } else { + data.isDeath = false; + } LOG_INFO("Parsed SMSG_DESTROY_OBJECT:"); LOG_INFO(" GUID: 0x", std::hex, data.guid, std::dec); @@ -2102,12 +2107,24 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data data.talentSpec = packet.readUInt8(); uint16_t spellCount = packet.readUInt16(); - LOG_INFO("SMSG_INITIAL_SPELLS: packetSize=", packetSize, " bytes, spellCount=", spellCount); + // Detect vanilla (uint16 spellId) vs WotLK (uint32 spellId) format + // Vanilla: 4 bytes/spell (uint16 id + uint16 slot), WotLK: 6 bytes/spell (uint32 id + uint16 unk) + size_t remainingAfterHeader = packetSize - 3; // subtract talentSpec(1) + spellCount(2) + bool vanillaFormat = remainingAfterHeader < static_cast(spellCount) * 6 + 2; + + LOG_INFO("SMSG_INITIAL_SPELLS: packetSize=", packetSize, " bytes, spellCount=", spellCount, + vanillaFormat ? " (vanilla uint16 format)" : " (WotLK uint32 format)"); data.spellIds.reserve(spellCount); for (uint16_t i = 0; i < spellCount; ++i) { - uint32_t spellId = packet.readUInt32(); - packet.readUInt16(); // unknown (always 0) + uint32_t spellId; + if (vanillaFormat) { + spellId = packet.readUInt16(); + packet.readUInt16(); // slot + } else { + spellId = packet.readUInt32(); + packet.readUInt16(); // unknown (always 0) + } if (spellId != 0) { data.spellIds.push_back(spellId); } @@ -2117,7 +2134,11 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data data.cooldowns.reserve(cooldownCount); for (uint16_t i = 0; i < cooldownCount; ++i) { SpellCooldownEntry entry; - entry.spellId = packet.readUInt32(); + if (vanillaFormat) { + entry.spellId = packet.readUInt16(); + } else { + entry.spellId = packet.readUInt32(); + } entry.itemId = packet.readUInt16(); entry.categoryId = packet.readUInt16(); entry.cooldownMs = packet.readUInt32(); diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index ddeb5cc9..cceb99ee 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -264,6 +264,7 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { std::vector dbcData; // Try expansion-specific CSV first (e.g. Data/expansions/wotlk/db/Spell.csv) + bool loadedFromCSV = false; if (!expansionDataPath_.empty()) { // Derive CSV name from DBC name: "Spell.dbc" -> "Spell.csv" std::string baseName = name; @@ -281,6 +282,7 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { dbcData.resize(static_cast(size)); f.read(reinterpret_cast(dbcData.data()), size); LOG_DEBUG("Found CSV DBC: ", csvPath); + loadedFromCSV = true; } } } @@ -298,8 +300,57 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { auto dbc = std::make_shared(); if (!dbc->load(dbcData)) { - LOG_ERROR("Failed to load DBC: ", name); - return nullptr; + // If CSV failed to parse, try binary DBC fallback + if (loadedFromCSV) { + LOG_WARNING("CSV DBC failed to parse: ", name, " — trying binary DBC fallback"); + dbcData.clear(); + std::string dbcPath = "DBFilesClient\\" + name; + dbcData = readFile(dbcPath); + if (!dbcData.empty()) { + dbc = std::make_shared(); + if (dbc->load(dbcData)) { + loadedFromCSV = false; + LOG_INFO("Binary DBC fallback succeeded: ", name); + } else { + LOG_ERROR("Failed to load DBC: ", name); + return nullptr; + } + } else { + LOG_ERROR("Failed to load DBC: ", name); + return nullptr; + } + } else { + LOG_ERROR("Failed to load DBC: ", name); + return nullptr; + } + } + + // Validate CSV-loaded DBCs: if >50 records but all have ID 0 in field 0, + // the CSV data is garbled (e.g. string data in the ID column). + if (loadedFromCSV && dbc->getRecordCount() > 50) { + uint32_t zeroIds = 0; + const uint32_t sampleSize = std::min(dbc->getRecordCount(), 100u); + for (uint32_t i = 0; i < sampleSize; ++i) { + if (dbc->getUInt32(i, 0) == 0) ++zeroIds; + } + // If >80% of sampled records have ID 0, the CSV is garbled + if (zeroIds > sampleSize * 4 / 5) { + LOG_WARNING("CSV DBC '", name, "' has garbled field 0 (", + zeroIds, "/", sampleSize, " records with ID=0) — falling back to binary DBC"); + dbcData.clear(); + std::string dbcPath = "DBFilesClient\\" + name; + dbcData = readFile(dbcPath); + if (!dbcData.empty()) { + dbc = std::make_shared(); + if (!dbc->load(dbcData)) { + LOG_ERROR("Binary DBC fallback also failed: ", name); + return nullptr; + } + LOG_INFO("Binary DBC fallback succeeded: ", name); + } else { + LOG_WARNING("No binary DBC fallback available for: ", name); + } + } } dbcCache[name] = dbc; diff --git a/src/pipeline/dbc_loader.cpp b/src/pipeline/dbc_loader.cpp index 13aca073..dd1d6f52 100644 --- a/src/pipeline/dbc_loader.cpp +++ b/src/pipeline/dbc_loader.cpp @@ -231,6 +231,12 @@ bool DBCFile::loadCSV(const std::vector& csvData) { } } + // Field 0 is always the numeric record ID in DBC files — never a string. + // Some CSV exports incorrectly mark it as a string column; force-remove it. + if (stringCols.erase(0) > 0) { + LOG_DEBUG("CSV DBC: removed field 0 from string columns (always numeric ID)"); + } + recordSize = fieldCount * 4; // --- Build string block with initial null byte --- @@ -288,7 +294,11 @@ bool DBCFile::loadCSV(const std::vector& csvData) { if (end == std::string::npos) end = line.size(); std::string tok = line.substr(pos, end - pos); if (!tok.empty()) { - row.fields[col] = static_cast(std::stoul(tok)); + try { + row.fields[col] = static_cast(std::stoul(tok)); + } catch (...) { + row.fields[col] = 0; // non-numeric value in numeric field + } } pos = (end < line.size()) ? end + 1 : line.size(); } diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index c069f71d..5f63248f 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -474,7 +474,7 @@ bool CharacterPreview::applyEquipment(const std::vector& eq if (recIdx < 0) continue; for (int region = 0; region < 8; region++) { - uint32_t fieldIdx = 15 + region; // texture_1..texture_8 + uint32_t fieldIdx = 14 + region; // texture_1..texture_8 std::string texName = displayInfoDbc->getString(static_cast(recIdx), fieldIdx); if (texName.empty()) continue; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 04fc7801..d27467e3 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2097,8 +2097,13 @@ const char* GameScreen::getChatTypeName(game::ChatType type) const { case game::ChatType::WHISPER: return "WHISPER"; case game::ChatType::WHISPER_INFORM: return "TO"; case game::ChatType::SYSTEM: return "SYSTEM"; + case game::ChatType::MONSTER_SAY: return "SAY"; + case game::ChatType::MONSTER_YELL: return "YELL"; + case game::ChatType::MONSTER_EMOTE: return "EMOTE"; case game::ChatType::CHANNEL: return "CHANNEL"; case game::ChatType::ACHIEVEMENT: return "ACHIEVEMENT"; + case game::ChatType::DND: return "DND"; + case game::ChatType::AFK: return "AFK"; default: return "UNKNOWN"; } } @@ -2135,6 +2140,12 @@ ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const { return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink case game::ChatType::SYSTEM: return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow + case game::ChatType::MONSTER_SAY: + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (same as SAY) + case game::ChatType::MONSTER_YELL: + return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red (same as YELL) + case game::ChatType::MONSTER_EMOTE: + return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE) case game::ChatType::CHANNEL: return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink case game::ChatType::ACHIEVEMENT: @@ -2314,10 +2325,10 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) { int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId); if (recIdx < 0) continue; - // DBC fields 15-22 = texture_1 through texture_8 (regions 0-7) - // (binary DBC has inventoryIcon_2 at field 6, shifting fields +1 vs CSV) + // DBC fields 14-21 = texture_1 through texture_8 (regions 0-7) + // Binary DBC (23 fields) has textures at 14+; some CSVs (25 fields) have them at 15+. for (int region = 0; region < 8; region++) { - uint32_t fieldIdx = 15 + region; + uint32_t fieldIdx = 14 + region; std::string texName = displayInfoDbc->getString(static_cast(recIdx), fieldIdx); if (texName.empty()) continue; @@ -2325,11 +2336,19 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) { // Try gender-specific first, then unisex fallback std::string base = "Item\\TextureComponents\\" + std::string(componentDirs[region]) + "\\" + texName; - std::string malePath = base + "_M.blp"; + // Determine gender suffix from active character + bool isFemale = false; + if (auto* gh = app.getGameHandler()) { + if (auto* ch = gh->getActiveCharacter()) { + isFemale = (ch->gender == game::Gender::FEMALE) || + (ch->gender == game::Gender::NONBINARY && ch->useFemaleModel); + } + } + std::string genderPath = base + (isFemale ? "_F.blp" : "_M.blp"); std::string unisexPath = base + "_U.blp"; std::string fullPath; - if (assetManager->fileExists(malePath)) { - fullPath = malePath; + if (assetManager->fileExists(genderPath)) { + fullPath = genderPath; } else if (assetManager->fileExists(unisexPath)) { fullPath = unisexPath; } else { diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 95088fab..ebaf98d3 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -258,7 +258,7 @@ void InventoryScreen::updatePreviewEquipment(game::Inventory& inventory) { if (recIdx < 0) continue; for (int region = 0; region < 8; region++) { - uint32_t fieldIdx = 15 + region; + uint32_t fieldIdx = 14 + region; std::string texName = displayInfoDbc->getString(static_cast(recIdx), fieldIdx); if (texName.empty()) continue;