diff --git a/include/auth/integrity.hpp b/include/auth/integrity.hpp index 1201455a..3ab5db79 100644 --- a/include/auth/integrity.hpp +++ b/include/auth/integrity.hpp @@ -24,10 +24,13 @@ bool computeIntegrityHashWin32(const std::array& checksumSalt, std::string& outError); // Same as computeIntegrityHashWin32, but allows selecting the EXE filename used in the file set. +// clientBuild controls which companion DLLs are included: Classic (<=6005) +// needs fmod/ijl15/dbghelp/unicows; TBC/WotLK clients did not ship those. bool computeIntegrityHashWin32WithExe(const std::array& checksumSalt, const std::vector& clientPublicKeyA, const std::string& miscDir, const std::string& exeName, + uint16_t clientBuild, std::array& outHash, std::string& outError); diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index 54e129f7..4abe1705 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -299,7 +299,7 @@ void AuthHandler::sendLogonProof() { for (const auto& dir : candidateDirs) { for (const char* exe : candidateExes) { std::string err; - if (computeIntegrityHashWin32WithExe(checksumSalt_, A, dir, exe, crcHash, err)) { + if (computeIntegrityHashWin32WithExe(checksumSalt_, A, dir, exe, clientInfo.build, crcHash, err)) { crcHashPtr = &crcHash; LOG_INFO("Integrity hash computed from ", dir, " (", exe, ")"); ok = true; diff --git a/src/auth/integrity.cpp b/src/auth/integrity.cpp index 13f71261..ee4cd46d 100644 --- a/src/auth/integrity.cpp +++ b/src/auth/integrity.cpp @@ -33,24 +33,29 @@ bool computeIntegrityHashWin32WithExe(const std::array& checksumSal const std::vector& clientPublicKeyA, const std::string& miscDir, const std::string& exeName, + uint16_t clientBuild, std::array& outHash, std::string& outError) { - // Files expected by 1.12.x Windows clients for the integrity check. - // If this needs to vary by build, make it data-driven in expansion.json later. + // Classic 1.12.x (build <=6005) ships fmod.dll, ijl15.dll, dbghelp.dll, + // unicows.dll alongside WoW.exe. TBC/WotLK clients do not; hashing + // only the .exe is sufficient (servers rarely validate the CRC for those). // // Turtle WoW ships a custom loader DLL. Some Turtle auth servers appear to validate integrity against // that distribution rather than a stock 1.12.1 client, so when using Turtle's executable we include // Turtle-specific DLLs as well. const bool isTurtleExe = (exeName == "TurtleWoW.exe"); + const bool isClassicBuild = (clientBuild <= 6005 || isTurtleExe); // Some macOS client layouts use FMOD dylib naming instead of fmod.dll. // We accept the first matching filename in each alias group. std::vector> fileGroups = { { exeName }, - { "fmod.dll", "fmod.dylib", "libfmod.dylib", "fmodex.dll", "fmodex.dylib", "libfmod.so" }, - { "ijl15.dll" }, - { "dbghelp.dll" }, - { "unicows.dll" }, }; + if (isClassicBuild) { + fileGroups.push_back({ "fmod.dll", "fmod.dylib", "libfmod.dylib", "fmodex.dll", "fmodex.dylib", "libfmod.so" }); + fileGroups.push_back({ "ijl15.dll" }); + fileGroups.push_back({ "dbghelp.dll" }); + fileGroups.push_back({ "unicows.dll" }); + } if (isTurtleExe) { fileGroups.push_back({ "twloader.dll" }); fileGroups.push_back({ "twdiscord.dll" }); @@ -108,7 +113,7 @@ bool computeIntegrityHashWin32(const std::array& checksumSalt, const std::string& miscDir, std::array& outHash, std::string& outError) { - return computeIntegrityHashWin32WithExe(checksumSalt, clientPublicKeyA, miscDir, "WoW.exe", outHash, outError); + return computeIntegrityHashWin32WithExe(checksumSalt, clientPublicKeyA, miscDir, "WoW.exe", 5875, outHash, outError); } } // namespace auth diff --git a/src/game/game_handler_packets.cpp b/src/game/game_handler_packets.cpp index 5a84bfe1..094d7b66 100644 --- a/src/game/game_handler_packets.cpp +++ b/src/game/game_handler_packets.cpp @@ -2804,13 +2804,36 @@ void GameHandler::handlePacket(network::Packet& packet) { auto logicalOp = opcodeTable_.fromWire(opcode); if (!logicalOp) { - static std::unordered_set loggedUnknownWireOpcodes; - if (loggedUnknownWireOpcodes.insert(opcode).second) { - LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec, - " state=", static_cast(state), - " size=", packet.getSize()); + // Login-critical opcodes share the same wire values across all expansions. + // Fall back to hardcoded mapping so the auth/char pipeline works even when + // the expansion opcode table failed to load (wrong CWD, missing Data/, etc.). + switch (opcode) { + case 0x1EC: logicalOp = Opcode::SMSG_AUTH_CHALLENGE; break; + case 0x1EE: logicalOp = Opcode::SMSG_AUTH_RESPONSE; break; + case 0x03B: logicalOp = Opcode::SMSG_CHAR_ENUM; break; + case 0x03A: logicalOp = Opcode::SMSG_CHAR_CREATE; break; + case 0x03C: logicalOp = Opcode::SMSG_CHAR_DELETE; break; + case 0x2E6: logicalOp = Opcode::SMSG_WARDEN_DATA; break; + default: break; + } + if (logicalOp) { + static bool loggedFallback = false; + if (!loggedFallback) { + loggedFallback = true; + LOG_WARNING("Opcode table lookup failed for login-critical opcode 0x", + std::hex, opcode, std::dec, + " (table has ", opcodeTable_.size(), " entries). " + "Using hardcoded fallback. Check that Data/expansions//opcodes.json is loadable."); + } + } else { + static std::unordered_set loggedUnknownWireOpcodes; + if (loggedUnknownWireOpcodes.insert(opcode).second) { + LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec, + " state=", static_cast(state), + " size=", packet.getSize()); + } + return; } - return; } // Dispatch via the opcode handler table diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index ad9f5908..53996666 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -218,13 +218,12 @@ bool OpcodeTable::loadFromJson(const std::string& path) { // Resolved JSON inheritance is the single source of truth for opcode mappings. // Load into a scratch map (the recursive loader supports add/remove via // _extends/_remove), then bake it into the flat vector for fast toWire(). - logicalToWire_.clear(); - logicalToWireSize_ = 0; - wireToLogical_.clear(); + // Load into temporaries so a failed reload doesn't wipe the working table. std::unordered_map scratch; + std::unordered_map newWireToLogical; std::unordered_set loadingStack; if (!loadOpcodeJsonRecursive(std::filesystem::path(path), - scratch, wireToLogical_, loadingStack) || + scratch, newWireToLogical, loadingStack) || scratch.empty()) { LOG_WARNING("OpcodeTable: no opcodes loaded from ", path); return false; @@ -236,10 +235,13 @@ bool OpcodeTable::loadFromJson(const std::string& path) { for (const auto& [logical, _wire] : scratch) { if (logical > maxIdx) maxIdx = logical; } - logicalToWire_.assign(static_cast(maxIdx) + 1, 0xFFFF); + std::vector newLogicalToWire(static_cast(maxIdx) + 1, 0xFFFF); for (const auto& [logical, wire] : scratch) { - logicalToWire_[logical] = wire; + newLogicalToWire[logical] = wire; } + + logicalToWire_ = std::move(newLogicalToWire); + wireToLogical_ = std::move(newWireToLogical); logicalToWireSize_ = scratch.size(); LOG_INFO("OpcodeTable: loaded ", logicalToWireSize_, " opcodes from ", path); diff --git a/tests/test_opcode_table.cpp b/tests/test_opcode_table.cpp index ce2a1cac..5735fba2 100644 --- a/tests/test_opcode_table.cpp +++ b/tests/test_opcode_table.cpp @@ -83,6 +83,25 @@ TEST_CASE("OpcodeTable loadFromJson nonexistent file", "[opcode_table]") { REQUIRE(table.size() == 0); } +TEST_CASE("OpcodeTable failed reload preserves existing data", "[opcode_table]") { + std::string json = R"({ "CMSG_PING": "0x1DC" })"; + auto path = writeTempJson(json); + + OpcodeTable table; + REQUIRE(table.loadFromJson(path)); + REQUIRE(table.size() == 1); + REQUIRE(table.toWire(LogicalOpcode::CMSG_PING) == 0x1DC); + + REQUIRE_FALSE(table.loadFromJson("/nonexistent/path/opcodes.json")); + REQUIRE(table.size() == 1); + REQUIRE(table.toWire(LogicalOpcode::CMSG_PING) == 0x1DC); + auto result = table.fromWire(0x1DC); + REQUIRE(result.has_value()); + REQUIRE(*result == LogicalOpcode::CMSG_PING); + + std::remove(path.c_str()); +} + TEST_CASE("OpcodeTable logicalToName returns enum name", "[opcode_table]") { const char* name = OpcodeTable::logicalToName(LogicalOpcode::CMSG_PING); REQUIRE(name != nullptr); diff --git a/tools/auth_login_probe/main.cpp b/tools/auth_login_probe/main.cpp index e458f6ad..b96329e2 100644 --- a/tools/auth_login_probe/main.cpp +++ b/tools/auth_login_probe/main.cpp @@ -222,7 +222,7 @@ int main(int argc, char** argv) { if (crcA == CrcAFormat::BigEndian) { std::reverse(crcABytes.begin(), crcABytes.end()); } - if (auth::computeIntegrityHashWin32WithExe(checksumSalt, crcABytes, miscDir, integrityExe, crcHash, err)) { + if (auth::computeIntegrityHashWin32WithExe(checksumSalt, crcABytes, miscDir, integrityExe, static_cast(build), crcHash, err)) { crcHashPtr = &crcHash; std::cerr << "Computed integrity hash using " << miscDir << " (" << integrityExe << ")\n"; } else {