Vanilla/Turtle WoW support: M2 loading, bone parsing, textures, auth

- Vanilla M2 bone struct (108 bytes) with 28-byte animation tracks
- Version-aware bone parsing (vanilla vs WotLK format detection)
- Fix CharSections.dbc field layout for vanilla (variation/color at 4-5)
- Remove broken CharSections.csv files (all fields marked as strings)
- Expansion data reload on profile switch (DBC cache clear, layout reload)
- Vanilla packet encryption (VanillaCrypt XOR-based header crypt)
- Extended character preview geoset range (0-99) for vanilla models
- DBC cache clear support in AssetManager
This commit is contained in:
Kelsi 2026-02-13 16:53:28 -08:00
parent 6729f66a37
commit 430c2bdcfa
34 changed files with 1066 additions and 24795 deletions

View file

@ -246,15 +246,17 @@ size_t TCPSocket::getExpectedPacketSize(uint8_t opcode) {
return 0; // Need more data to determine
case 0x01: // LOGON_PROOF response
// Success: opcode(1) + status(1) + M2(20) + accountFlags(4) + surveyId(4) + loginFlags(2) = 32
// Some vanilla-era servers send a shorter success response:
// opcode(1) + status(1) + M2(20) = 22
// Success response varies by server build (determined by client build sent in challenge):
// Build >= 8089: cmd(1)+error(1)+M2(20)+accountFlags(4)+surveyId(4)+loginFlags(2) = 32
// Build 6299-8088: cmd(1)+error(1)+M2(20)+surveyId(4)+loginFlags(2) = 28
// Build < 6299: cmd(1)+error(1)+M2(20)+surveyId(4) = 26
// Failure: varies by server — minimum 2 bytes (opcode + status), some send 4
if (receiveBuffer.size() >= 2) {
uint8_t status = receiveBuffer[1];
if (status == 0x00) {
if (receiveBuffer.size() >= 32) return 32; // WotLK-style
if (receiveBuffer.size() >= 22) return 22; // minimal/vanilla-style
if (receiveBuffer.size() >= 32) return 32;
if (receiveBuffer.size() >= 28) return 28;
if (receiveBuffer.size() >= 26) return 26;
return 0;
} else {
// Consume up to 4 bytes if available, minimum 2

View file

@ -115,6 +115,7 @@ void WorldSocket::disconnect() {
}
connected = false;
encryptionEnabled = false;
useVanillaCrypt = false;
receiveBuffer.clear();
headerBytesDecrypted = 0;
LOG_INFO("Disconnected from world server");
@ -214,7 +215,11 @@ void WorldSocket::send(const Packet& packet) {
// Encrypt header if encryption is enabled (all 6 bytes)
if (encryptionEnabled) {
encryptCipher.process(sendData.data(), 6);
if (useVanillaCrypt) {
vanillaCrypt.encrypt(sendData.data(), 6);
} else {
encryptCipher.process(sendData.data(), 6);
}
}
// Add payload (unencrypted)
@ -291,17 +296,26 @@ void WorldSocket::update() {
}
if (receivedAny) {
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
" recv call(s), buffered=", receiveBuffer.size());
LOG_INFO("World socket read ", bytesReadThisTick, " bytes in ", readOps,
" recv call(s), buffered=", receiveBuffer.size());
// Hex dump received bytes for auth debugging
if (bytesReadThisTick <= 128) {
std::string hex;
for (size_t i = 0; i < receiveBuffer.size(); ++i) {
char buf[4]; snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]); hex += buf;
}
LOG_INFO("World socket raw bytes: ", hex);
}
tryParsePackets();
if (connected && !receiveBuffer.empty()) {
LOG_DEBUG("World socket parse left ", receiveBuffer.size(),
" bytes buffered (awaiting complete packet)");
LOG_INFO("World socket parse left ", receiveBuffer.size(),
" bytes buffered (awaiting complete packet)");
}
}
if (sawClose) {
LOG_INFO("World server connection closed");
LOG_INFO("World server connection closed (receivedAny=", receivedAny,
" buffered=", receiveBuffer.size(), ")");
disconnect();
return;
}
@ -317,7 +331,11 @@ void WorldSocket::tryParsePackets() {
// Only decrypt bytes we haven't already decrypted
if (encryptionEnabled && headerBytesDecrypted < 4) {
size_t toDecrypt = 4 - headerBytesDecrypted;
decryptCipher.process(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
if (useVanillaCrypt) {
vanillaCrypt.decrypt(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
} else {
decryptCipher.process(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
}
headerBytesDecrypted = 4;
}
@ -402,33 +420,36 @@ void WorldSocket::tryParsePackets() {
}
}
void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey) {
void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build) {
if (sessionKey.size() != 40) {
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
return;
}
LOG_INFO(">>> ENABLING ENCRYPTION - encryptionEnabled will become true <<<");
// Vanilla/TBC (build <= 8606) uses XOR+addition cipher with raw session key
// WotLK (build > 8606) uses HMAC-SHA1 derived RC4 with 1024-byte drop
useVanillaCrypt = (build <= 8606);
// Convert hardcoded keys to vectors
std::vector<uint8_t> encryptKey(ENCRYPT_KEY, ENCRYPT_KEY + 16);
std::vector<uint8_t> decryptKey(DECRYPT_KEY, DECRYPT_KEY + 16);
LOG_INFO(">>> ENABLING ENCRYPTION (", useVanillaCrypt ? "vanilla XOR" : "WotLK RC4",
") build=", build, " <<<");
// Compute HMAC-SHA1(seed, sessionKey) for each cipher
// The 16-byte seed is the HMAC key, session key is the message
std::vector<uint8_t> encryptHash = auth::Crypto::hmacSHA1(encryptKey, sessionKey);
std::vector<uint8_t> decryptHash = auth::Crypto::hmacSHA1(decryptKey, sessionKey);
if (useVanillaCrypt) {
vanillaCrypt.init(sessionKey);
} else {
// WotLK: HMAC-SHA1(hardcoded seed, sessionKey) → RC4 key
std::vector<uint8_t> encryptKey(ENCRYPT_KEY, ENCRYPT_KEY + 16);
std::vector<uint8_t> decryptKey(DECRYPT_KEY, DECRYPT_KEY + 16);
LOG_DEBUG("Encrypt hash: ", encryptHash.size(), " bytes");
LOG_DEBUG("Decrypt hash: ", decryptHash.size(), " bytes");
std::vector<uint8_t> encryptHash = auth::Crypto::hmacSHA1(encryptKey, sessionKey);
std::vector<uint8_t> decryptHash = auth::Crypto::hmacSHA1(decryptKey, sessionKey);
// Initialize RC4 ciphers with HMAC results
encryptCipher.init(encryptHash);
decryptCipher.init(decryptHash);
encryptCipher.init(encryptHash);
decryptCipher.init(decryptHash);
// Drop first 1024 bytes of keystream (WoW protocol requirement)
encryptCipher.drop(1024);
decryptCipher.drop(1024);
// Drop first 1024 bytes of keystream (WoW WotLK protocol requirement)
encryptCipher.drop(1024);
decryptCipher.drop(1024);
}
encryptionEnabled = true;
headerTracePacketsLeft = 24;