mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
6729f66a37
commit
430c2bdcfa
34 changed files with 1066 additions and 24795 deletions
|
|
@ -95,6 +95,7 @@ set(WOWEE_SOURCES
|
|||
src/auth/big_num.cpp
|
||||
src/auth/crypto.cpp
|
||||
src/auth/rc4.cpp
|
||||
src/auth/vanilla_crypt.cpp
|
||||
|
||||
# Game
|
||||
src/game/expansion_profile.cpp
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,8 +9,8 @@
|
|||
},
|
||||
"CharSections": {
|
||||
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
||||
"Texture1": 4, "Texture2": 5, "Texture3": 6,
|
||||
"VariationIndex": 8, "ColorIndex": 9
|
||||
"VariationIndex": 4, "ColorIndex": 5,
|
||||
"Texture1": 6, "Texture2": 7, "Texture3": 8
|
||||
},
|
||||
"SpellIcon": { "ID": 0, "Path": 1 },
|
||||
"FactionTemplate": {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -9,8 +9,8 @@
|
|||
},
|
||||
"CharSections": {
|
||||
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
||||
"Texture1": 4, "Texture2": 5, "Texture3": 6,
|
||||
"VariationIndex": 8, "ColorIndex": 9
|
||||
"VariationIndex": 4, "ColorIndex": 5,
|
||||
"Texture1": 6, "Texture2": 7, "Texture3": 8
|
||||
},
|
||||
"SpellIcon": { "ID": 0, "Path": 1 },
|
||||
"FactionTemplate": {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
"id": "turtle",
|
||||
"name": "Turtle WoW",
|
||||
"shortName": "Turtle",
|
||||
"version": { "major": 1, "minor": 12, "patch": 1 },
|
||||
"build": 5875,
|
||||
"version": { "major": 1, "minor": 18, "patch": 0 },
|
||||
"build": 7234,
|
||||
"worldBuild": 5875,
|
||||
"protocolVersion": 3,
|
||||
"os": "OSX",
|
||||
"locale": "enGB",
|
||||
"maxLevel": 60,
|
||||
"races": [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -130,7 +130,8 @@ struct RealmListResponse {
|
|||
// REALM_LIST response parser
|
||||
class RealmListResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, RealmListResponse& response);
|
||||
// protocolVersion: 3 = vanilla (uint8 realmCount, uint32 icon), 8 = WotLK (uint16 realmCount, uint8 icon)
|
||||
static bool parse(network::Packet& packet, RealmListResponse& response, uint8_t protocolVersion = 8);
|
||||
};
|
||||
|
||||
} // namespace auth
|
||||
|
|
|
|||
58
include/auth/vanilla_crypt.hpp
Normal file
58
include/auth/vanilla_crypt.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
|
||||
/**
|
||||
* Vanilla/TBC WoW Header Cipher
|
||||
*
|
||||
* Used for encrypting/decrypting World of Warcraft packet headers
|
||||
* in vanilla (1.x) and TBC (2.x) clients. This is a simple XOR+addition
|
||||
* chaining cipher that uses the raw 40-byte SRP session key directly.
|
||||
*
|
||||
* Algorithm (encrypt):
|
||||
* encrypted = (plaintext ^ key[index]) + previousEncrypted
|
||||
* index = (index + 1) % keyLen
|
||||
*
|
||||
* Algorithm (decrypt):
|
||||
* plaintext = (encrypted - previousEncrypted) ^ key[index]
|
||||
* index = (index + 1) % keyLen
|
||||
*/
|
||||
class VanillaCrypt {
|
||||
public:
|
||||
VanillaCrypt() = default;
|
||||
~VanillaCrypt() = default;
|
||||
|
||||
/**
|
||||
* Initialize the cipher with the raw session key
|
||||
* @param sessionKey 40-byte session key from SRP auth
|
||||
*/
|
||||
void init(const std::vector<uint8_t>& sessionKey);
|
||||
|
||||
/**
|
||||
* Encrypt outgoing header bytes (CMSG: 6 bytes)
|
||||
* @param data Pointer to header data to encrypt in-place
|
||||
* @param length Number of bytes to encrypt
|
||||
*/
|
||||
void encrypt(uint8_t* data, size_t length);
|
||||
|
||||
/**
|
||||
* Decrypt incoming header bytes (SMSG: 4 bytes)
|
||||
* @param data Pointer to header data to decrypt in-place
|
||||
* @param length Number of bytes to decrypt
|
||||
*/
|
||||
void decrypt(uint8_t* data, size_t length);
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> key_;
|
||||
uint8_t sendIndex_ = 0;
|
||||
uint8_t sendPrev_ = 0;
|
||||
uint8_t recvIndex_ = 0;
|
||||
uint8_t recvPrev_ = 0;
|
||||
};
|
||||
|
||||
} // namespace auth
|
||||
} // namespace wowee
|
||||
|
|
@ -57,6 +57,7 @@ public:
|
|||
game::ExpansionRegistry* getExpansionRegistry() { return expansionRegistry_.get(); }
|
||||
pipeline::DBCLayout* getDBCLayout() { return dbcLayout_.get(); }
|
||||
pipeline::HDPackManager* getHDPackManager() { return hdPackManager_.get(); }
|
||||
void reloadExpansionData(); // Reload DBC layouts, opcodes, etc. after expansion change
|
||||
|
||||
// Singleton access
|
||||
static Application& getInstance() { return *instance; }
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ struct ExpansionProfile {
|
|||
uint8_t majorVersion = 0;
|
||||
uint8_t minorVersion = 0;
|
||||
uint8_t patchVersion = 0;
|
||||
uint16_t build = 0;
|
||||
uint8_t protocolVersion = 0; // SRP auth protocol version byte
|
||||
uint16_t build = 0; // Realm build (sent in LOGON_CHALLENGE)
|
||||
uint16_t worldBuild = 0; // World build (sent in CMSG_AUTH_SESSION, defaults to build)
|
||||
uint8_t protocolVersion = 0; // SRP auth protocol version byte
|
||||
// Client header fields used in LOGON_CHALLENGE.
|
||||
// Defaults match a typical Windows x86 client.
|
||||
std::string game = "WoW";
|
||||
|
|
|
|||
|
|
@ -1214,6 +1214,19 @@ private:
|
|||
std::unique_ptr<WardenCrypto> wardenCrypto_;
|
||||
std::unique_ptr<WardenModuleManager> wardenModuleManager_;
|
||||
|
||||
// Warden module download state
|
||||
enum class WardenState {
|
||||
WAIT_MODULE_USE, // Waiting for first SMSG (MODULE_USE)
|
||||
WAIT_MODULE_CACHE, // Sent MODULE_MISSING, receiving module chunks
|
||||
WAIT_HASH_REQUEST, // Module received, waiting for HASH_REQUEST
|
||||
WAIT_CHECKS, // Hash sent, waiting for check requests
|
||||
};
|
||||
WardenState wardenState_ = WardenState::WAIT_MODULE_USE;
|
||||
std::vector<uint8_t> wardenModuleHash_; // 16 bytes MD5
|
||||
std::vector<uint8_t> wardenModuleKey_; // 16 bytes RC4
|
||||
uint32_t wardenModuleSize_ = 0;
|
||||
std::vector<uint8_t> wardenModuleData_; // Downloaded module chunks
|
||||
|
||||
// ---- XP tracking ----
|
||||
uint32_t playerXp_ = 0;
|
||||
uint32_t playerNextLevelXp_ = 0;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ namespace wowee {
|
|||
namespace game {
|
||||
|
||||
/**
|
||||
* Warden anti-cheat crypto handler for WoW 3.3.5a
|
||||
* Handles RC4 encryption/decryption of Warden packets
|
||||
* Warden anti-cheat crypto handler.
|
||||
* Derives RC4 keys from the 40-byte SRP session key using SHA1Randx,
|
||||
* then encrypts/decrypts Warden packet payloads.
|
||||
*/
|
||||
class WardenCrypto {
|
||||
public:
|
||||
|
|
@ -17,45 +18,40 @@ public:
|
|||
~WardenCrypto();
|
||||
|
||||
/**
|
||||
* Initialize Warden crypto with module seed
|
||||
* @param moduleData The SMSG_WARDEN_DATA payload containing seed
|
||||
* @return true if initialization succeeded
|
||||
* Initialize Warden crypto from the 40-byte SRP session key.
|
||||
* Derives encrypt (client->server) and decrypt (server->client) RC4 keys
|
||||
* using the SHA1Randx / WardenKeyGenerator algorithm.
|
||||
*/
|
||||
bool initialize(const std::vector<uint8_t>& moduleData);
|
||||
bool initFromSessionKey(const std::vector<uint8_t>& sessionKey);
|
||||
|
||||
/**
|
||||
* Decrypt an incoming Warden packet
|
||||
* @param data Encrypted data from server
|
||||
* @return Decrypted data
|
||||
* Replace RC4 keys (called after module hash challenge succeeds).
|
||||
* @param newEncryptKey 16-byte key for encrypting outgoing packets
|
||||
* @param newDecryptKey 16-byte key for decrypting incoming packets
|
||||
*/
|
||||
void replaceKeys(const std::vector<uint8_t>& newEncryptKey,
|
||||
const std::vector<uint8_t>& newDecryptKey);
|
||||
|
||||
/** Decrypt an incoming SMSG_WARDEN_DATA payload. */
|
||||
std::vector<uint8_t> decrypt(const std::vector<uint8_t>& data);
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing Warden response
|
||||
* @param data Plaintext response data
|
||||
* @return Encrypted data
|
||||
*/
|
||||
/** Encrypt an outgoing CMSG_WARDEN_DATA payload. */
|
||||
std::vector<uint8_t> encrypt(const std::vector<uint8_t>& data);
|
||||
|
||||
/**
|
||||
* Check if crypto has been initialized
|
||||
*/
|
||||
bool isInitialized() const { return initialized_; }
|
||||
|
||||
private:
|
||||
bool initialized_;
|
||||
std::vector<uint8_t> inputKey_;
|
||||
std::vector<uint8_t> outputKey_;
|
||||
|
||||
// RC4 state for incoming packets
|
||||
std::vector<uint8_t> inputRC4State_;
|
||||
uint8_t inputRC4_i_;
|
||||
uint8_t inputRC4_j_;
|
||||
// RC4 state for decrypting incoming packets (server->client)
|
||||
std::vector<uint8_t> decryptRC4State_;
|
||||
uint8_t decryptRC4_i_;
|
||||
uint8_t decryptRC4_j_;
|
||||
|
||||
// RC4 state for outgoing packets
|
||||
std::vector<uint8_t> outputRC4State_;
|
||||
uint8_t outputRC4_i_;
|
||||
uint8_t outputRC4_j_;
|
||||
// RC4 state for encrypting outgoing packets (client->server)
|
||||
std::vector<uint8_t> encryptRC4State_;
|
||||
uint8_t encryptRC4_i_;
|
||||
uint8_t encryptRC4_j_;
|
||||
|
||||
void initRC4(const std::vector<uint8_t>& key,
|
||||
std::vector<uint8_t>& state,
|
||||
|
|
@ -63,6 +59,14 @@ private:
|
|||
|
||||
void processRC4(const uint8_t* input, uint8_t* output, size_t length,
|
||||
std::vector<uint8_t>& state, uint8_t& i, uint8_t& j);
|
||||
|
||||
/**
|
||||
* SHA1Randx / WardenKeyGenerator: generates pseudo-random bytes from a seed.
|
||||
* Used to derive the 16-byte encrypt and decrypt keys from the session key.
|
||||
*/
|
||||
static void sha1RandxGenerate(const std::vector<uint8_t>& seed,
|
||||
uint8_t* outputEncryptKey,
|
||||
uint8_t* outputDecryptKey);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ struct AuthChallengeData {
|
|||
uint32_t serverSeed; // Random seed from server
|
||||
// Note: 3.3.5a has additional data after this
|
||||
|
||||
bool isValid() const { return unknown1 != 0; }
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "network/packet.hpp"
|
||||
#include "network/net_platform.hpp"
|
||||
#include "auth/rc4.hpp"
|
||||
#include "auth/vanilla_crypt.hpp"
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
|
@ -14,12 +15,13 @@ namespace network {
|
|||
/**
|
||||
* World Server Socket
|
||||
*
|
||||
* Handles WoW 3.3.5a world server protocol with RC4 header encryption.
|
||||
* Handles WoW world server protocol with header encryption.
|
||||
* Supports both vanilla/TBC (XOR+addition cipher) and WotLK (RC4).
|
||||
*
|
||||
* 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)
|
||||
* - Headers are RC4-encrypted after CMSG_AUTH_SESSION
|
||||
* - Headers are encrypted after CMSG_AUTH_SESSION
|
||||
* - Packet bodies remain unencrypted
|
||||
* - Size field includes opcode bytes (payloadLen = size - 2)
|
||||
*/
|
||||
|
|
@ -56,12 +58,13 @@ public:
|
|||
}
|
||||
|
||||
/**
|
||||
* Initialize RC4 encryption for packet headers
|
||||
* Initialize header encryption for packet headers
|
||||
* Must be called after CMSG_AUTH_SESSION before further communication
|
||||
*
|
||||
* @param sessionKey 40-byte session key from auth server
|
||||
* @param build Client build number (determines cipher: <= 8606 = XOR, > 8606 = RC4)
|
||||
*/
|
||||
void initEncryption(const std::vector<uint8_t>& sessionKey);
|
||||
void initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build = 12340);
|
||||
|
||||
/**
|
||||
* Check if header encryption is enabled
|
||||
|
|
@ -77,10 +80,14 @@ private:
|
|||
socket_t sockfd = INVALID_SOCK;
|
||||
bool connected = false;
|
||||
bool encryptionEnabled = false;
|
||||
bool useVanillaCrypt = false; // true = XOR cipher, false = RC4
|
||||
|
||||
// RC4 ciphers for header encryption/decryption
|
||||
auth::RC4 encryptCipher; // For outgoing headers
|
||||
auth::RC4 decryptCipher; // For incoming headers
|
||||
// WotLK RC4 ciphers for header encryption/decryption
|
||||
auth::RC4 encryptCipher;
|
||||
auth::RC4 decryptCipher;
|
||||
|
||||
// Vanilla/TBC XOR+addition cipher
|
||||
auth::VanillaCrypt vanillaCrypt;
|
||||
|
||||
// Receive buffer
|
||||
std::vector<uint8_t> receiveBuffer;
|
||||
|
|
|
|||
|
|
@ -131,6 +131,11 @@ public:
|
|||
*/
|
||||
void clearCache();
|
||||
|
||||
/**
|
||||
* Clear only DBC cache (forces reload on next loadDBC call)
|
||||
*/
|
||||
void clearDBCCache();
|
||||
|
||||
private:
|
||||
bool initialized = false;
|
||||
std::string dataPath;
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ void AuthHandler::handleRealmListResponse(network::Packet& packet) {
|
|||
LOG_DEBUG("Handling REALM_LIST response");
|
||||
|
||||
RealmListResponse response;
|
||||
if (!RealmListResponseParser::parse(packet, response)) {
|
||||
if (!RealmListResponseParser::parse(packet, response, clientInfo.protocolVersion)) {
|
||||
LOG_ERROR("Failed to parse REALM_LIST response");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,30 +25,26 @@ network::Packet LogonChallengePacket::build(const std::string& account, const Cl
|
|||
// Payload size
|
||||
packet.writeUInt16(payloadSize);
|
||||
|
||||
// Write a 4-byte ASCII field (FourCC-ish): bytes are sent in-order and null-padded.
|
||||
// Auth servers expect literal "x86\\0", "Win\\0", "enUS"/"enGB", etc.
|
||||
// Write a 4-byte FourCC field with reversed string characters.
|
||||
// The auth server reads these as a C-string (stops at first null), then
|
||||
// reverses the string. So we must send the characters reversed and
|
||||
// null-padded on the right. E.g. "Win" → bytes ['n','i','W',0x00].
|
||||
// Server reads "niW", reverses → "Win", stores "Win".
|
||||
auto writeFourCC = [&packet](const std::string& str) {
|
||||
uint8_t buf[4] = {0, 0, 0, 0};
|
||||
size_t len = std::min<size_t>(4, str.length());
|
||||
// Write string characters in reverse order, then null-pad
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
buf[i] = static_cast<uint8_t>(str[i]);
|
||||
buf[i] = static_cast<uint8_t>(str[len - 1 - i]);
|
||||
}
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
packet.writeUInt8(buf[i]);
|
||||
}
|
||||
};
|
||||
|
||||
// Game name (4 bytes, big-endian FourCC — NOT reversed)
|
||||
// "WoW" → "WoW\0" on the wire
|
||||
{
|
||||
uint8_t buf[4] = {0, 0, 0, 0};
|
||||
for (size_t i = 0; i < std::min<size_t>(4, info.game.length()); ++i) {
|
||||
buf[i] = static_cast<uint8_t>(info.game[i]);
|
||||
}
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
packet.writeUInt8(buf[i]);
|
||||
}
|
||||
}
|
||||
// Game name (4 bytes, reversed FourCC)
|
||||
// "WoW" → bytes ['W','o','W',0x00] on the wire
|
||||
writeFourCC(info.game);
|
||||
|
||||
// Version (3 bytes)
|
||||
packet.writeUInt8(info.majorVersion);
|
||||
|
|
@ -189,6 +185,7 @@ network::Packet LogonProofPacket::buildLegacy(const std::vector<uint8_t>& A,
|
|||
for (int i = 0; i < 20; ++i) packet.writeUInt8(0); // CRC hash
|
||||
}
|
||||
packet.writeUInt8(0); // number of keys
|
||||
packet.writeUInt8(0); // security flags
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
@ -292,8 +289,9 @@ network::Packet RealmListPacket::build() {
|
|||
return packet;
|
||||
}
|
||||
|
||||
bool RealmListResponseParser::parse(network::Packet& packet, RealmListResponse& response) {
|
||||
bool RealmListResponseParser::parse(network::Packet& packet, RealmListResponse& response, uint8_t protocolVersion) {
|
||||
// Note: opcode byte already consumed by handlePacket()
|
||||
const bool isVanilla = (protocolVersion < 8);
|
||||
|
||||
// Packet size (2 bytes) - we already know the size, skip it
|
||||
uint16_t packetSize = packet.readUInt16();
|
||||
|
|
@ -302,8 +300,13 @@ bool RealmListResponseParser::parse(network::Packet& packet, RealmListResponse&
|
|||
// Unknown uint32
|
||||
packet.readUInt32();
|
||||
|
||||
// Realm count
|
||||
uint16_t realmCount = packet.readUInt16();
|
||||
// Realm count: uint8 for vanilla/classic, uint16 for WotLK+
|
||||
uint16_t realmCount;
|
||||
if (isVanilla) {
|
||||
realmCount = packet.readUInt8();
|
||||
} else {
|
||||
realmCount = packet.readUInt16();
|
||||
}
|
||||
LOG_INFO("REALM_LIST response: ", realmCount, " realms");
|
||||
|
||||
response.realms.clear();
|
||||
|
|
@ -312,11 +315,19 @@ bool RealmListResponseParser::parse(network::Packet& packet, RealmListResponse&
|
|||
for (uint16_t i = 0; i < realmCount; ++i) {
|
||||
Realm realm;
|
||||
|
||||
// Icon
|
||||
realm.icon = packet.readUInt8();
|
||||
// Icon/type: uint32 for vanilla, uint8 for WotLK
|
||||
if (isVanilla) {
|
||||
realm.icon = static_cast<uint8_t>(packet.readUInt32());
|
||||
} else {
|
||||
realm.icon = packet.readUInt8();
|
||||
}
|
||||
|
||||
// Lock
|
||||
realm.lock = packet.readUInt8();
|
||||
// Lock: not present in vanilla (added in TBC)
|
||||
if (!isVanilla) {
|
||||
realm.lock = packet.readUInt8();
|
||||
} else {
|
||||
realm.lock = 0;
|
||||
}
|
||||
|
||||
// Flags
|
||||
realm.flags = packet.readUInt8();
|
||||
|
|
|
|||
34
src/auth/vanilla_crypt.cpp
Normal file
34
src/auth/vanilla_crypt.cpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#include "auth/vanilla_crypt.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
|
||||
void VanillaCrypt::init(const std::vector<uint8_t>& sessionKey) {
|
||||
key_ = sessionKey;
|
||||
sendIndex_ = 0;
|
||||
sendPrev_ = 0;
|
||||
recvIndex_ = 0;
|
||||
recvPrev_ = 0;
|
||||
}
|
||||
|
||||
void VanillaCrypt::encrypt(uint8_t* data, size_t length) {
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
uint8_t x = (data[i] ^ key_[sendIndex_]) + sendPrev_;
|
||||
sendIndex_ = (sendIndex_ + 1) % key_.size();
|
||||
data[i] = x;
|
||||
sendPrev_ = x;
|
||||
}
|
||||
}
|
||||
|
||||
void VanillaCrypt::decrypt(uint8_t* data, size_t length) {
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
uint8_t enc = data[i];
|
||||
uint8_t x = (enc - recvPrev_) ^ key_[recvIndex_];
|
||||
recvIndex_ = (recvIndex_ + 1) % key_.size();
|
||||
recvPrev_ = enc;
|
||||
data[i] = x;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace auth
|
||||
} // namespace wowee
|
||||
|
|
@ -148,6 +148,7 @@ bool Application::initialize() {
|
|||
if (profile) {
|
||||
std::string opcodesPath = profile->dataPath + "/opcodes.json";
|
||||
if (!gameHandler->getOpcodeTable().loadFromJson(opcodesPath)) {
|
||||
gameHandler->getOpcodeTable().loadWotlkDefaults();
|
||||
LOG_INFO("Using built-in WotLK opcode defaults");
|
||||
}
|
||||
game::setActiveOpcodeTable(&gameHandler->getOpcodeTable());
|
||||
|
|
@ -155,6 +156,7 @@ bool Application::initialize() {
|
|||
// Load expansion-specific update field table
|
||||
std::string updateFieldsPath = profile->dataPath + "/update_fields.json";
|
||||
if (!gameHandler->getUpdateFieldTable().loadFromJson(updateFieldsPath)) {
|
||||
gameHandler->getUpdateFieldTable().loadWotlkDefaults();
|
||||
LOG_INFO("Using built-in WotLK update field defaults");
|
||||
}
|
||||
game::setActiveUpdateFieldTable(&gameHandler->getUpdateFieldTable());
|
||||
|
|
@ -458,6 +460,42 @@ void Application::setState(AppState newState) {
|
|||
}
|
||||
}
|
||||
|
||||
void Application::reloadExpansionData() {
|
||||
if (!expansionRegistry_ || !gameHandler) return;
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
if (!profile) return;
|
||||
|
||||
LOG_INFO("Reloading expansion data for: ", profile->name);
|
||||
|
||||
std::string opcodesPath = profile->dataPath + "/opcodes.json";
|
||||
if (!gameHandler->getOpcodeTable().loadFromJson(opcodesPath)) {
|
||||
gameHandler->getOpcodeTable().loadWotlkDefaults();
|
||||
}
|
||||
game::setActiveOpcodeTable(&gameHandler->getOpcodeTable());
|
||||
|
||||
std::string updateFieldsPath = profile->dataPath + "/update_fields.json";
|
||||
if (!gameHandler->getUpdateFieldTable().loadFromJson(updateFieldsPath)) {
|
||||
gameHandler->getUpdateFieldTable().loadWotlkDefaults();
|
||||
}
|
||||
game::setActiveUpdateFieldTable(&gameHandler->getUpdateFieldTable());
|
||||
|
||||
gameHandler->setPacketParsers(game::createPacketParsers(profile->id));
|
||||
|
||||
if (dbcLayout_) {
|
||||
std::string dbcLayoutsPath = profile->dataPath + "/dbc_layouts.json";
|
||||
if (!dbcLayout_->loadFromJson(dbcLayoutsPath)) {
|
||||
dbcLayout_->loadWotlkDefaults();
|
||||
}
|
||||
pipeline::setActiveDBCLayout(dbcLayout_.get());
|
||||
}
|
||||
|
||||
// Update expansion data path for CSV DBC lookups and clear DBC cache
|
||||
if (assetManager && !profile->dataPath.empty()) {
|
||||
assetManager->setExpansionDataPath(profile->dataPath);
|
||||
assetManager->clearDBCCache();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::logoutToLogin() {
|
||||
LOG_INFO("Logout requested");
|
||||
if (gameHandler) {
|
||||
|
|
@ -908,7 +946,7 @@ void Application::setupUICallbacks() {
|
|||
uint32_t clientBuild = 12340; // default WotLK
|
||||
if (expansionRegistry_) {
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
if (profile) clientBuild = profile->build;
|
||||
if (profile) clientBuild = profile->worldBuild;
|
||||
}
|
||||
if (gameHandler->connect(host, port, sessionKey, accountName, clientBuild, realmId)) {
|
||||
LOG_INFO("Connected to world server, transitioning to character selection");
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::stri
|
|||
}
|
||||
|
||||
p.build = static_cast<uint16_t>(jsonInt(json, "build"));
|
||||
p.worldBuild = static_cast<uint16_t>(jsonInt(json, "worldBuild", p.build));
|
||||
p.protocolVersion = static_cast<uint8_t>(jsonInt(json, "protocolVersion"));
|
||||
// Optional client header fields (LOGON_CHALLENGE)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -136,6 +136,13 @@ bool GameHandler::connect(const std::string& host,
|
|||
this->accountName = accountName;
|
||||
this->build = build;
|
||||
this->realmId_ = realmId;
|
||||
|
||||
// Diagnostic: dump session key for AUTH_REJECT debugging
|
||||
{
|
||||
std::string hex;
|
||||
for (uint8_t b : sessionKey) { char buf[4]; snprintf(buf, sizeof(buf), "%02x", b); hex += buf; }
|
||||
LOG_INFO("GameHandler session key (", sessionKey.size(), "): ", hex);
|
||||
}
|
||||
requiresWarden_ = false;
|
||||
wardenGateSeen_ = false;
|
||||
wardenGateElapsed_ = 0.0f;
|
||||
|
|
@ -1448,7 +1455,7 @@ void GameHandler::sendAuthSession() {
|
|||
// AzerothCore enables encryption before sending AUTH_RESPONSE,
|
||||
// so we need to be ready to decrypt the response
|
||||
LOG_INFO("Enabling encryption immediately after AUTH_SESSION");
|
||||
socket->initEncryption(sessionKey);
|
||||
socket->initEncryption(sessionKey, build);
|
||||
|
||||
setState(WorldState::AUTH_SENT);
|
||||
LOG_INFO("CMSG_AUTH_SESSION sent, encryption enabled, waiting for AUTH_RESPONSE...");
|
||||
|
|
@ -1529,7 +1536,15 @@ void GameHandler::handleCharEnum(network::Packet& packet) {
|
|||
LOG_INFO("Handling SMSG_CHAR_ENUM");
|
||||
|
||||
CharEnumResponse response;
|
||||
if (!CharEnumParser::parse(packet, response)) {
|
||||
bool parsed = false;
|
||||
if (build <= 6005) {
|
||||
// Vanilla 1.12.x format (different equipment layout, no customization flag)
|
||||
ClassicPacketParsers classicParser;
|
||||
parsed = classicParser.parseCharEnum(packet, response);
|
||||
} else {
|
||||
parsed = CharEnumParser::parse(packet, response);
|
||||
}
|
||||
if (!parsed) {
|
||||
fail("Failed to parse SMSG_CHAR_ENUM");
|
||||
return;
|
||||
}
|
||||
|
|
@ -1901,290 +1916,232 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
wardenPacketsAfterGate_ = 0;
|
||||
}
|
||||
|
||||
// Log the raw encrypted packet
|
||||
std::string hex;
|
||||
hex.reserve(data.size() * 3);
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
char b[4];
|
||||
snprintf(b, sizeof(b), "%02x ", data[i]);
|
||||
hex += b;
|
||||
}
|
||||
LOG_INFO("Received SMSG_WARDEN_DATA (len=", data.size(), ", raw: ", hex, ")");
|
||||
|
||||
// Initialize crypto on first packet (usually module load)
|
||||
// Initialize Warden crypto from session key on first packet
|
||||
if (!wardenCrypto_) {
|
||||
wardenCrypto_ = std::make_unique<WardenCrypto>();
|
||||
if (!wardenCrypto_->initialize(data)) {
|
||||
LOG_ERROR("Warden: Failed to initialize crypto");
|
||||
if (sessionKey.size() != 40) {
|
||||
LOG_ERROR("Warden: No valid session key (size=", sessionKey.size(), "), cannot init crypto");
|
||||
wardenCrypto_.reset();
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Warden: Crypto initialized, analyzing module structure");
|
||||
|
||||
// Parse module structure (37 bytes typical):
|
||||
// [1 byte opcode][16 bytes seed][20 bytes challenge/hash]
|
||||
std::vector<uint8_t> trailingBytes;
|
||||
uint8_t opcodeByte = data[0];
|
||||
|
||||
if (data.size() >= 37) {
|
||||
std::string trailingHex;
|
||||
for (size_t i = 17; i < 37; ++i) {
|
||||
trailingBytes.push_back(data[i]);
|
||||
char b[4];
|
||||
snprintf(b, sizeof(b), "%02x ", data[i]);
|
||||
trailingHex += b;
|
||||
}
|
||||
LOG_INFO("Warden: Opcode byte: 0x", std::hex, (int)opcodeByte, std::dec);
|
||||
LOG_INFO("Warden: Challenge/hash (20 bytes): ", trailingHex);
|
||||
if (!wardenCrypto_->initFromSessionKey(sessionKey)) {
|
||||
LOG_ERROR("Warden: Failed to initialize crypto from session key");
|
||||
wardenCrypto_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// For opcode 0xF6, try empty response (some servers expect nothing)
|
||||
std::vector<uint8_t> moduleResponse;
|
||||
|
||||
LOG_INFO("Warden: Trying response strategy: Empty response (0 bytes)");
|
||||
|
||||
// Send empty/null response
|
||||
// moduleResponse remains empty (size = 0)
|
||||
|
||||
LOG_INFO("Warden: Crypto initialized, sending MODULE_OK with challenge");
|
||||
LOG_INFO("Warden: Opcode 0x01 + 20-byte challenge");
|
||||
|
||||
// Try opcode 0x01 (MODULE_OK) followed by 20-byte challenge
|
||||
std::vector<uint8_t> hashResponse;
|
||||
hashResponse.push_back(0x01); // WARDEN_CMSG_MODULE_OK
|
||||
|
||||
// Append the 20-byte challenge
|
||||
for (size_t i = 0; i < trailingBytes.size(); ++i) {
|
||||
hashResponse.push_back(trailingBytes[i]);
|
||||
}
|
||||
|
||||
LOG_INFO("Warden: SHA1 hash computed (20 bytes), total response: ", hashResponse.size(), " bytes");
|
||||
|
||||
// Encrypt the response
|
||||
std::vector<uint8_t> encryptedResponse = wardenCrypto_->encrypt(hashResponse);
|
||||
|
||||
// Send HASH_RESULT response
|
||||
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
|
||||
for (uint8_t byte : encryptedResponse) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
|
||||
if (socket && socket->isConnected()) {
|
||||
socket->send(response);
|
||||
LOG_INFO("Sent CMSG_WARDEN_DATA MODULE_OK+challenge (", encryptedResponse.size(), " bytes encrypted)");
|
||||
}
|
||||
|
||||
// Mark that we've seen the initial seed packet
|
||||
wardenGateSeen_ = true;
|
||||
|
||||
return;
|
||||
wardenState_ = WardenState::WAIT_MODULE_USE;
|
||||
}
|
||||
|
||||
// Decrypt the packet
|
||||
// Decrypt the payload
|
||||
std::vector<uint8_t> decrypted = wardenCrypto_->decrypt(data);
|
||||
|
||||
// Log decrypted data (first 64 bytes for readability)
|
||||
std::string decHex;
|
||||
size_t logSize = std::min(decrypted.size(), size_t(64));
|
||||
decHex.reserve(logSize * 3);
|
||||
for (size_t i = 0; i < logSize; ++i) {
|
||||
char b[4];
|
||||
snprintf(b, sizeof(b), "%02x ", decrypted[i]);
|
||||
decHex += b;
|
||||
}
|
||||
if (decrypted.size() > 64) {
|
||||
decHex += "... (" + std::to_string(decrypted.size() - 64) + " more bytes)";
|
||||
}
|
||||
LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", decHex);
|
||||
|
||||
// Check if this looks like a module download (large size)
|
||||
if (decrypted.size() > 256) {
|
||||
LOG_INFO("Warden: Received large packet (", decrypted.size(), " bytes) - attempting module load");
|
||||
|
||||
// Try to load this as a Warden module
|
||||
// Compute MD5 hash of the decrypted data for module identification
|
||||
std::vector<uint8_t> moduleMD5 = auth::Crypto::md5(decrypted);
|
||||
|
||||
// The data is already decrypted by our crypto layer, but the module loader
|
||||
// expects encrypted data. We need to pass the original encrypted data.
|
||||
// For now, try loading with what we have.
|
||||
auto module = wardenModuleManager_->getModule(moduleMD5);
|
||||
|
||||
// Extract RC4 key from current crypto state (we already initialized it)
|
||||
std::vector<uint8_t> dummyKey(16, 0); // Module will use existing crypto
|
||||
|
||||
if (module->load(decrypted, moduleMD5, dummyKey)) {
|
||||
LOG_INFO("Warden: ✓ Module loaded successfully!");
|
||||
// Module is now ready to process check requests
|
||||
// No response needed for module download
|
||||
return;
|
||||
} else {
|
||||
LOG_WARNING("Warden: ✗ Module load failed, falling back to fake responses");
|
||||
// Log decrypted data
|
||||
{
|
||||
std::string hex;
|
||||
size_t logSize = std::min(decrypted.size(), size_t(64));
|
||||
hex.reserve(logSize * 3);
|
||||
for (size_t i = 0; i < logSize; ++i) {
|
||||
char b[4]; snprintf(b, sizeof(b), "%02x ", decrypted[i]); hex += b;
|
||||
}
|
||||
if (decrypted.size() > 64)
|
||||
hex += "... (" + std::to_string(decrypted.size() - 64) + " more)";
|
||||
LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", hex);
|
||||
}
|
||||
|
||||
// Prepare response data
|
||||
std::vector<uint8_t> responseData;
|
||||
|
||||
if (decrypted.empty()) {
|
||||
LOG_INFO("Warden: Empty decrypted packet");
|
||||
} else {
|
||||
uint8_t opcode = decrypted[0];
|
||||
|
||||
// If we have a loaded module, try to use it for check processing
|
||||
std::vector<uint8_t> moduleMD5 = auth::Crypto::md5(decrypted);
|
||||
auto module = wardenModuleManager_->getModule(moduleMD5);
|
||||
|
||||
if (module && module->isLoaded()) {
|
||||
LOG_INFO("Warden: Using loaded module to process check request (opcode 0x",
|
||||
std::hex, (int)opcode, std::dec, ")");
|
||||
|
||||
if (module->processCheckRequest(decrypted, responseData)) {
|
||||
LOG_INFO("Warden: ✓ Module generated response (", responseData.size(), " bytes)");
|
||||
// Response will be encrypted and sent below
|
||||
} else {
|
||||
LOG_WARNING("Warden: ✗ Module failed to process check, using fallback");
|
||||
// Fall through to fake responses
|
||||
}
|
||||
}
|
||||
|
||||
// If module processing didn't generate a response, use fake responses
|
||||
if (responseData.empty()) {
|
||||
uint8_t opcode = decrypted[0];
|
||||
|
||||
// Warden check opcodes (from WoW 3.3.5a protocol)
|
||||
switch (opcode) {
|
||||
case 0x00: // Module info request
|
||||
LOG_INFO("Warden: Module info request");
|
||||
responseData.push_back(0x00);
|
||||
break;
|
||||
|
||||
case 0x01: { // Hash request
|
||||
LOG_INFO("Warden: Hash request (file/memory hash check)");
|
||||
// Parse hash request structure
|
||||
// Format: [0x01][seed][hash_to_check][...]
|
||||
responseData.push_back(0x01);
|
||||
|
||||
// For each hash check, respond with matching result
|
||||
if (decrypted.size() > 1) {
|
||||
// Extract number of hash checks (varies by server)
|
||||
size_t pos = 1;
|
||||
while (pos < decrypted.size() && responseData.size() < 64) {
|
||||
responseData.push_back(0x00); // Hash matches (no violation)
|
||||
pos += 20; // Skip 20-byte hash (SHA1)
|
||||
}
|
||||
} else {
|
||||
responseData.push_back(0x00); // Pass
|
||||
}
|
||||
LOG_INFO("Warden: Hash check response with ", responseData.size() - 1, " results");
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x02: { // Lua string check
|
||||
LOG_INFO("Warden: Lua string check (anti-addon detection)");
|
||||
// Format: [0x02][lua_length][lua_string]
|
||||
responseData.push_back(0x02);
|
||||
|
||||
// Return empty string = no suspicious Lua found
|
||||
responseData.push_back(0x00); // String length = 0
|
||||
LOG_INFO("Warden: Lua check - returned empty (no detection)");
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x05: { // Memory/page check
|
||||
LOG_INFO("Warden: Memory page check (anti-cheat scan)");
|
||||
// Format: [0x05][seed][address_count][addresses...][length]
|
||||
|
||||
if (decrypted.size() >= 2) {
|
||||
uint8_t numChecks = decrypted[1];
|
||||
LOG_INFO("Warden: Processing ", (int)numChecks, " memory checks");
|
||||
|
||||
responseData.push_back(0x05);
|
||||
|
||||
// For each memory region checked, return "clean" data
|
||||
for (uint8_t i = 0; i < numChecks; ++i) {
|
||||
// Return zeroed memory (appears clean/unmodified)
|
||||
responseData.push_back(0x00);
|
||||
}
|
||||
|
||||
LOG_INFO("Warden: Memory check - all regions clean");
|
||||
} else {
|
||||
responseData.push_back(0x05);
|
||||
responseData.push_back(0x00);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x04: { // Timing check
|
||||
LOG_INFO("Warden: Timing check (detect speedhacks)");
|
||||
// Return current timestamp
|
||||
responseData.push_back(0x04);
|
||||
uint32_t timestamp = static_cast<uint32_t>(std::time(nullptr));
|
||||
responseData.push_back((timestamp >> 0) & 0xFF);
|
||||
responseData.push_back((timestamp >> 8) & 0xFF);
|
||||
responseData.push_back((timestamp >> 16) & 0xFF);
|
||||
responseData.push_back((timestamp >> 24) & 0xFF);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOG_INFO("Warden: Unknown check opcode 0x", std::hex, (int)opcode, std::dec);
|
||||
LOG_INFO("Warden: Packet dump: ", decHex);
|
||||
|
||||
// Try to detect packet type from size/structure
|
||||
if (decrypted.size() > 20) {
|
||||
LOG_INFO("Warden: Large packet - might be batched checks");
|
||||
// Some servers send multiple checks in one packet
|
||||
// Try to respond to each one
|
||||
responseData.push_back(0x00); // Generic "OK" response
|
||||
} else {
|
||||
// Small unknown packet - echo with success
|
||||
responseData.push_back(opcode);
|
||||
responseData.push_back(0x00);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have no response data, don't send anything
|
||||
if (responseData.empty()) {
|
||||
LOG_INFO("Warden: No response generated (module loaded or waiting for checks)");
|
||||
LOG_WARNING("Warden: Empty decrypted payload");
|
||||
return;
|
||||
}
|
||||
|
||||
// Log plaintext response
|
||||
std::string respPlainHex;
|
||||
respPlainHex.reserve(responseData.size() * 3);
|
||||
for (uint8_t byte : responseData) {
|
||||
char b[4];
|
||||
snprintf(b, sizeof(b), "%02x ", byte);
|
||||
respPlainHex += b;
|
||||
}
|
||||
LOG_INFO("Warden: Response plaintext (", responseData.size(), " bytes): ", respPlainHex);
|
||||
uint8_t wardenOpcode = decrypted[0];
|
||||
|
||||
// Encrypt response
|
||||
std::vector<uint8_t> encrypted = wardenCrypto_->encrypt(responseData);
|
||||
// Helper to send an encrypted Warden response
|
||||
auto sendWardenResponse = [&](const std::vector<uint8_t>& plaintext) {
|
||||
std::vector<uint8_t> encrypted = wardenCrypto_->encrypt(plaintext);
|
||||
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
|
||||
for (uint8_t byte : encrypted) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
if (socket && socket->isConnected()) {
|
||||
socket->send(response);
|
||||
LOG_INFO("Warden: Sent response (", plaintext.size(), " bytes plaintext)");
|
||||
}
|
||||
};
|
||||
|
||||
// Log encrypted response
|
||||
std::string respEncHex;
|
||||
respEncHex.reserve(encrypted.size() * 3);
|
||||
for (uint8_t byte : encrypted) {
|
||||
char b[4];
|
||||
snprintf(b, sizeof(b), "%02x ", byte);
|
||||
respEncHex += b;
|
||||
}
|
||||
LOG_INFO("Warden: Response encrypted (", encrypted.size(), " bytes): ", respEncHex);
|
||||
switch (wardenOpcode) {
|
||||
case 0x00: { // WARDEN_SMSG_MODULE_USE
|
||||
// Format: [1 opcode][16 moduleHash][16 moduleKey][4 moduleSize]
|
||||
if (decrypted.size() < 37) {
|
||||
LOG_ERROR("Warden: MODULE_USE too short (", decrypted.size(), " bytes, need 37)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build and send response packet
|
||||
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
|
||||
for (uint8_t byte : encrypted) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
wardenModuleHash_.assign(decrypted.begin() + 1, decrypted.begin() + 17);
|
||||
wardenModuleKey_.assign(decrypted.begin() + 17, decrypted.begin() + 33);
|
||||
wardenModuleSize_ = static_cast<uint32_t>(decrypted[33])
|
||||
| (static_cast<uint32_t>(decrypted[34]) << 8)
|
||||
| (static_cast<uint32_t>(decrypted[35]) << 16)
|
||||
| (static_cast<uint32_t>(decrypted[36]) << 24);
|
||||
wardenModuleData_.clear();
|
||||
|
||||
if (socket && socket->isConnected()) {
|
||||
socket->send(response);
|
||||
LOG_INFO("Sent CMSG_WARDEN_DATA encrypted response");
|
||||
{
|
||||
std::string hashHex, keyHex;
|
||||
for (auto b : wardenModuleHash_) { char s[4]; snprintf(s, 4, "%02x", b); hashHex += s; }
|
||||
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_);
|
||||
}
|
||||
|
||||
// Respond with MODULE_MISSING (opcode 0x00) to request the module data
|
||||
std::vector<uint8_t> resp = { 0x00 }; // WARDEN_CMSG_MODULE_MISSING
|
||||
sendWardenResponse(resp);
|
||||
wardenState_ = WardenState::WAIT_MODULE_CACHE;
|
||||
LOG_INFO("Warden: Sent MODULE_MISSING, waiting for module data chunks");
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x01: { // WARDEN_SMSG_MODULE_CACHE (module data chunk)
|
||||
// Format: [1 opcode][2 chunkSize LE][chunkSize bytes data]
|
||||
if (decrypted.size() < 3) {
|
||||
LOG_ERROR("Warden: MODULE_CACHE too short");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t chunkSize = static_cast<uint16_t>(decrypted[1])
|
||||
| (static_cast<uint16_t>(decrypted[2]) << 8);
|
||||
|
||||
if (decrypted.size() < 3u + chunkSize) {
|
||||
LOG_ERROR("Warden: MODULE_CACHE chunk truncated (claimed ", chunkSize,
|
||||
", have ", decrypted.size() - 3, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
wardenModuleData_.insert(wardenModuleData_.end(),
|
||||
decrypted.begin() + 3,
|
||||
decrypted.begin() + 3 + chunkSize);
|
||||
|
||||
LOG_INFO("Warden: MODULE_CACHE chunk ", chunkSize, " bytes, total ",
|
||||
wardenModuleData_.size(), "/", wardenModuleSize_);
|
||||
|
||||
// Check if module download is complete
|
||||
if (wardenModuleData_.size() >= wardenModuleSize_) {
|
||||
LOG_INFO("Warden: Module download complete (",
|
||||
wardenModuleData_.size(), " bytes)");
|
||||
wardenState_ = WardenState::WAIT_HASH_REQUEST;
|
||||
|
||||
// Send MODULE_OK (opcode 0x01)
|
||||
std::vector<uint8_t> resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK
|
||||
sendWardenResponse(resp);
|
||||
LOG_INFO("Warden: Sent MODULE_OK");
|
||||
}
|
||||
// No response for intermediate chunks
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x05: { // WARDEN_SMSG_HASH_REQUEST
|
||||
// Format: [1 opcode][16 seed]
|
||||
if (decrypted.size() < 17) {
|
||||
LOG_ERROR("Warden: HASH_REQUEST too short (", decrypted.size(), " bytes, need 17)");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> seed(decrypted.begin() + 1, decrypted.begin() + 17);
|
||||
{
|
||||
std::string seedHex;
|
||||
for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; }
|
||||
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<uint8_t> moduleDecrypted(wardenModuleData_.size());
|
||||
{
|
||||
// RC4 KSA + PRGA with wardenModuleKey_
|
||||
std::vector<uint8_t> S(256);
|
||||
for (int i = 0; i < 256; ++i) S[i] = static_cast<uint8_t>(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];
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Warden: Module decrypted, computing hash response");
|
||||
|
||||
// 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<uint8_t> 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);
|
||||
|
||||
// Send HASH_RESULT (WARDEN_CMSG_HASH_RESULT = opcode 0x04)
|
||||
std::vector<uint8_t> 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<uint8_t> hashInput;
|
||||
hashInput.insert(hashInput.end(), seed.begin(), seed.end());
|
||||
auto hash = auth::Crypto::sha1(hashInput);
|
||||
|
||||
std::vector<uint8_t> resp;
|
||||
resp.push_back(0x04);
|
||||
resp.insert(resp.end(), hash.begin(), hash.end());
|
||||
sendWardenResponse(resp);
|
||||
}
|
||||
|
||||
wardenState_ = WardenState::WAIT_CHECKS;
|
||||
break;
|
||||
}
|
||||
|
||||
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
|
||||
std::vector<uint8_t> 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
|
||||
sendWardenResponse(resp);
|
||||
LOG_INFO("Warden: Sent CHEAT_CHECKS_RESULT (minimal)");
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOG_INFO("Warden: Unknown opcode 0x", std::hex, (int)wardenOpcode, std::dec,
|
||||
" (state=", (int)wardenState_, ", size=", decrypted.size(), ")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -572,6 +572,10 @@ bool OpcodeTable::loadFromJson(const std::string& path) {
|
|||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
// Save old tables so we can restore on failure
|
||||
auto savedLogicalToWire = logicalToWire_;
|
||||
auto savedWireToLogical = wireToLogical_;
|
||||
|
||||
logicalToWire_.clear();
|
||||
wireToLogical_.clear();
|
||||
|
||||
|
|
@ -624,8 +628,15 @@ bool OpcodeTable::loadFromJson(const std::string& path) {
|
|||
pos = valEnd + 1;
|
||||
}
|
||||
|
||||
if (loaded == 0) {
|
||||
LOG_WARNING("OpcodeTable: no opcodes loaded from ", path, ", restoring previous tables");
|
||||
logicalToWire_ = std::move(savedLogicalToWire);
|
||||
wireToLogical_ = std::move(savedWireToLogical);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("OpcodeTable: loaded ", loaded, " opcodes from ", path);
|
||||
return loaded > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t OpcodeTable::toWire(LogicalOpcode op) const {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ bool UpdateFieldTable::loadFromJson(const std::string& path) {
|
|||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
auto savedFieldMap = fieldMap_;
|
||||
fieldMap_.clear();
|
||||
size_t loaded = 0;
|
||||
size_t pos = 0;
|
||||
|
|
@ -139,8 +140,14 @@ bool UpdateFieldTable::loadFromJson(const std::string& path) {
|
|||
pos = valEnd + 1;
|
||||
}
|
||||
|
||||
if (loaded == 0) {
|
||||
LOG_WARNING("UpdateFieldTable: no fields loaded from ", path, ", restoring previous table");
|
||||
fieldMap_ = std::move(savedFieldMap);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("UpdateFieldTable: loaded ", loaded, " fields from ", path);
|
||||
return loaded > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t UpdateFieldTable::index(UF field) const {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "game/warden_crypto.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
|
@ -6,65 +7,109 @@
|
|||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
// Warden module keys for WoW 3.3.5a (from client analysis)
|
||||
// These are the standard keys used by most 3.3.5a servers
|
||||
static const uint8_t WARDEN_MODULE_KEY[16] = {
|
||||
0xC5, 0x35, 0xB2, 0x1E, 0xF8, 0xE7, 0x9F, 0x4B,
|
||||
0x91, 0xB6, 0xD1, 0x34, 0xA7, 0x2F, 0x58, 0x8C
|
||||
};
|
||||
|
||||
WardenCrypto::WardenCrypto()
|
||||
: initialized_(false)
|
||||
, inputRC4_i_(0)
|
||||
, inputRC4_j_(0)
|
||||
, outputRC4_i_(0)
|
||||
, outputRC4_j_(0) {
|
||||
, decryptRC4_i_(0)
|
||||
, decryptRC4_j_(0)
|
||||
, encryptRC4_i_(0)
|
||||
, encryptRC4_j_(0) {
|
||||
}
|
||||
|
||||
WardenCrypto::~WardenCrypto() {
|
||||
}
|
||||
|
||||
bool WardenCrypto::initialize(const std::vector<uint8_t>& moduleData) {
|
||||
if (moduleData.empty()) {
|
||||
LOG_ERROR("Warden: Cannot initialize with empty module data");
|
||||
void WardenCrypto::sha1RandxGenerate(const std::vector<uint8_t>& seed,
|
||||
uint8_t* outputEncryptKey,
|
||||
uint8_t* outputDecryptKey) {
|
||||
// SHA1Randx / WardenKeyGenerator algorithm (from MaNGOS/VMaNGOS)
|
||||
// Split the 40-byte session key in half
|
||||
size_t half = seed.size() / 2;
|
||||
|
||||
// o1 = SHA1(first half of session key)
|
||||
std::vector<uint8_t> firstHalf(seed.begin(), seed.begin() + half);
|
||||
auto o1 = auth::Crypto::sha1(firstHalf);
|
||||
|
||||
// o2 = SHA1(second half of session key)
|
||||
std::vector<uint8_t> secondHalf(seed.begin() + half, seed.end());
|
||||
auto o2 = auth::Crypto::sha1(secondHalf);
|
||||
|
||||
// o0 = 20 zero bytes initially
|
||||
std::vector<uint8_t> o0(20, 0);
|
||||
|
||||
uint32_t taken = 20; // Force FillUp on first Generate
|
||||
|
||||
// Lambda: FillUp - compute o0 = SHA1(o1 || o0 || o2)
|
||||
auto fillUp = [&]() {
|
||||
std::vector<uint8_t> combined;
|
||||
combined.reserve(60);
|
||||
combined.insert(combined.end(), o1.begin(), o1.end());
|
||||
combined.insert(combined.end(), o0.begin(), o0.end());
|
||||
combined.insert(combined.end(), o2.begin(), o2.end());
|
||||
o0 = auth::Crypto::sha1(combined);
|
||||
taken = 0;
|
||||
};
|
||||
|
||||
// Generate first 16 bytes → encrypt key (client→server)
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (taken == 20) fillUp();
|
||||
outputEncryptKey[i] = o0[taken++];
|
||||
}
|
||||
|
||||
// Generate next 16 bytes → decrypt key (server→client)
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (taken == 20) fillUp();
|
||||
outputDecryptKey[i] = o0[taken++];
|
||||
}
|
||||
}
|
||||
|
||||
bool WardenCrypto::initFromSessionKey(const std::vector<uint8_t>& sessionKey) {
|
||||
if (sessionKey.size() != 40) {
|
||||
LOG_ERROR("Warden: Session key must be 40 bytes, got ", sessionKey.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Warden: Initializing crypto with ", moduleData.size(), " byte module");
|
||||
uint8_t encryptKey[16];
|
||||
uint8_t decryptKey[16];
|
||||
sha1RandxGenerate(sessionKey, encryptKey, decryptKey);
|
||||
|
||||
// Warden 3.3.5a module format (typically):
|
||||
// [1 byte opcode][16 bytes seed/key][remaining bytes = encrypted module data]
|
||||
|
||||
if (moduleData.size() < 17) {
|
||||
LOG_WARNING("Warden: Module too small (", moduleData.size(), " bytes), using default keys");
|
||||
// Use default keys
|
||||
inputKey_.assign(WARDEN_MODULE_KEY, WARDEN_MODULE_KEY + 16);
|
||||
outputKey_.assign(WARDEN_MODULE_KEY, WARDEN_MODULE_KEY + 16);
|
||||
} else {
|
||||
// Extract seed from module (skip first opcode byte)
|
||||
inputKey_.assign(moduleData.begin() + 1, moduleData.begin() + 17);
|
||||
outputKey_ = inputKey_;
|
||||
|
||||
// XOR with module key for output
|
||||
for (size_t i = 0; i < 16; ++i) {
|
||||
outputKey_[i] ^= WARDEN_MODULE_KEY[i];
|
||||
// Log derived keys
|
||||
{
|
||||
std::string hex;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
char b[4]; snprintf(b, sizeof(b), "%02x ", encryptKey[i]); hex += b;
|
||||
}
|
||||
|
||||
LOG_INFO("Warden: Extracted 16-byte seed from module");
|
||||
LOG_INFO("Warden: Encrypt key (C→S): ", hex);
|
||||
hex.clear();
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
char b[4]; snprintf(b, sizeof(b), "%02x ", decryptKey[i]); hex += b;
|
||||
}
|
||||
LOG_INFO("Warden: Decrypt key (S→C): ", hex);
|
||||
}
|
||||
|
||||
// Initialize RC4 ciphers
|
||||
inputRC4State_.resize(256);
|
||||
outputRC4State_.resize(256);
|
||||
std::vector<uint8_t> ek(encryptKey, encryptKey + 16);
|
||||
std::vector<uint8_t> dk(decryptKey, decryptKey + 16);
|
||||
|
||||
initRC4(inputKey_, inputRC4State_, inputRC4_i_, inputRC4_j_);
|
||||
initRC4(outputKey_, outputRC4State_, outputRC4_i_, outputRC4_j_);
|
||||
encryptRC4State_.resize(256);
|
||||
decryptRC4State_.resize(256);
|
||||
|
||||
initRC4(ek, encryptRC4State_, encryptRC4_i_, encryptRC4_j_);
|
||||
initRC4(dk, decryptRC4State_, decryptRC4_i_, decryptRC4_j_);
|
||||
|
||||
initialized_ = true;
|
||||
LOG_INFO("Warden: Crypto initialized successfully");
|
||||
LOG_INFO("Warden: Crypto initialized from session key");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WardenCrypto::replaceKeys(const std::vector<uint8_t>& newEncryptKey,
|
||||
const std::vector<uint8_t>& newDecryptKey) {
|
||||
encryptRC4State_.resize(256);
|
||||
decryptRC4State_.resize(256);
|
||||
initRC4(newEncryptKey, encryptRC4State_, encryptRC4_i_, encryptRC4_j_);
|
||||
initRC4(newDecryptKey, decryptRC4State_, decryptRC4_i_, decryptRC4_j_);
|
||||
LOG_INFO("Warden: RC4 keys replaced (module-specific keys)");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> WardenCrypto::decrypt(const std::vector<uint8_t>& data) {
|
||||
if (!initialized_) {
|
||||
LOG_WARNING("Warden: Attempted to decrypt without initialization");
|
||||
|
|
@ -73,7 +118,7 @@ std::vector<uint8_t> WardenCrypto::decrypt(const std::vector<uint8_t>& data) {
|
|||
|
||||
std::vector<uint8_t> result(data.size());
|
||||
processRC4(data.data(), result.data(), data.size(),
|
||||
inputRC4State_, inputRC4_i_, inputRC4_j_);
|
||||
decryptRC4State_, decryptRC4_i_, decryptRC4_j_);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -85,19 +130,17 @@ std::vector<uint8_t> WardenCrypto::encrypt(const std::vector<uint8_t>& data) {
|
|||
|
||||
std::vector<uint8_t> result(data.size());
|
||||
processRC4(data.data(), result.data(), data.size(),
|
||||
outputRC4State_, outputRC4_i_, outputRC4_j_);
|
||||
encryptRC4State_, encryptRC4_i_, encryptRC4_j_);
|
||||
return result;
|
||||
}
|
||||
|
||||
void WardenCrypto::initRC4(const std::vector<uint8_t>& key,
|
||||
std::vector<uint8_t>& state,
|
||||
uint8_t& i, uint8_t& j) {
|
||||
// Initialize permutation
|
||||
for (int idx = 0; idx < 256; ++idx) {
|
||||
state[idx] = static_cast<uint8_t>(idx);
|
||||
}
|
||||
|
||||
// Key scheduling algorithm (KSA)
|
||||
j = 0;
|
||||
for (int idx = 0; idx < 256; ++idx) {
|
||||
j = (j + state[idx] + key[idx % key.size()]) & 0xFF;
|
||||
|
|
|
|||
|
|
@ -59,11 +59,11 @@ network::Packet AuthSessionPacket::build(uint32_t build,
|
|||
packet.writeString(upperAccount);
|
||||
packet.writeUInt32(0); // LoginServerType
|
||||
packet.writeUInt32(clientSeed);
|
||||
// Some private cores validate these fields against realmlist/worldserver settings.
|
||||
// Default to 1/1 and realmId (falling back to 1) rather than all zeros.
|
||||
packet.writeUInt32(1); // RegionID
|
||||
packet.writeUInt32(1); // BattlegroupID
|
||||
packet.writeUInt32(realmId ? realmId : 1); // RealmID
|
||||
// AzerothCore ignores these fields; other cores may validate them.
|
||||
// Use 0 for maximum compatibility.
|
||||
packet.writeUInt32(0); // RegionID
|
||||
packet.writeUInt32(0); // BattlegroupID
|
||||
packet.writeUInt32(realmId); // RealmID
|
||||
LOG_DEBUG(" Realm ID: ", realmId);
|
||||
packet.writeUInt32(0); // DOS response (uint64)
|
||||
packet.writeUInt32(0);
|
||||
|
|
@ -148,10 +148,36 @@ std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
|
|||
// Session key (40 bytes)
|
||||
hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end());
|
||||
|
||||
LOG_DEBUG("Auth hash input: ", hashInput.size(), " bytes");
|
||||
// Diagnostic: dump auth hash inputs for debugging AUTH_REJECT
|
||||
{
|
||||
auto toHex = [](const uint8_t* data, size_t len) {
|
||||
std::string s;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
LOG_INFO("AUTH HASH: account='", accountName, "' clientSeed=0x", std::hex, clientSeed,
|
||||
" serverSeed=0x", serverSeed, std::dec);
|
||||
LOG_INFO("AUTH HASH: sessionKey=", toHex(sessionKey.data(), sessionKey.size()));
|
||||
LOG_INFO("AUTH HASH: input(", hashInput.size(), ")=", toHex(hashInput.data(), hashInput.size()));
|
||||
}
|
||||
|
||||
// Compute SHA1 hash
|
||||
return auth::Crypto::sha1(hashInput);
|
||||
auto result = auth::Crypto::sha1(hashInput);
|
||||
|
||||
{
|
||||
auto toHex = [](const uint8_t* data, size_t len) {
|
||||
std::string s;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
LOG_INFO("AUTH HASH: digest=", toHex(result.data(), result.size()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -396,6 +396,12 @@ std::vector<uint8_t> AssetManager::readFileOptional(const std::string& path) con
|
|||
return readFile(path);
|
||||
}
|
||||
|
||||
void AssetManager::clearDBCCache() {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex);
|
||||
dbcCache.clear();
|
||||
LOG_INFO("Cleared DBC cache");
|
||||
}
|
||||
|
||||
void AssetManager::clearCache() {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex);
|
||||
dbcCache.clear();
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ struct FBlockDisk {
|
|||
uint32_t ofsKeys;
|
||||
};
|
||||
|
||||
// Full M2 bone structure (on-disk, 88 bytes)
|
||||
// Full M2 bone structure (on-disk, 88 bytes for WotLK)
|
||||
struct M2BoneDisk {
|
||||
int32_t keyBoneId; // 4
|
||||
uint32_t flags; // 4
|
||||
|
|
@ -151,7 +151,31 @@ struct M2BoneDisk {
|
|||
float pivot[3]; // 12
|
||||
}; // Total: 88
|
||||
|
||||
// M2 animation sequence structure
|
||||
// Vanilla M2 animation track header (on-disk, 28 bytes — has extra ranges M2Array)
|
||||
struct M2TrackDiskVanilla {
|
||||
uint16_t interpolationType; // 2
|
||||
int16_t globalSequence; // 2
|
||||
uint32_t nRanges; // 4 — extra in vanilla (animation sequence ranges)
|
||||
uint32_t ofsRanges; // 4 — extra in vanilla
|
||||
uint32_t nTimestamps; // 4
|
||||
uint32_t ofsTimestamps; // 4
|
||||
uint32_t nKeys; // 4
|
||||
uint32_t ofsKeys; // 4
|
||||
}; // Total: 28
|
||||
|
||||
// Vanilla M2 bone structure (on-disk, 108 bytes — no boneNameCRC, 28-byte tracks)
|
||||
struct M2BoneDiskVanilla {
|
||||
int32_t keyBoneId; // 4
|
||||
uint32_t flags; // 4
|
||||
int16_t parentBone; // 2
|
||||
uint16_t submeshId; // 2
|
||||
M2TrackDiskVanilla translation; // 28
|
||||
M2TrackDiskVanilla rotation; // 28
|
||||
M2TrackDiskVanilla scale; // 28
|
||||
float pivot[3]; // 12
|
||||
}; // Total: 108
|
||||
|
||||
// M2 animation sequence structure (WotLK, 64 bytes)
|
||||
struct M2SequenceDisk {
|
||||
uint16_t id;
|
||||
uint16_t variationIndex;
|
||||
|
|
@ -169,6 +193,25 @@ struct M2SequenceDisk {
|
|||
uint16_t aliasNext;
|
||||
};
|
||||
|
||||
// Vanilla M2 animation sequence (68 bytes — has start_timestamp before duration)
|
||||
struct M2SequenceDiskVanilla {
|
||||
uint16_t id;
|
||||
uint16_t variationIndex;
|
||||
uint32_t startTimestamp; // Extra field in vanilla (removed in WotLK)
|
||||
uint32_t endTimestamp; // Becomes 'duration' in WotLK
|
||||
float movingSpeed;
|
||||
uint32_t flags;
|
||||
int16_t frequency;
|
||||
uint16_t padding;
|
||||
uint32_t replayMin;
|
||||
uint32_t replayMax;
|
||||
uint32_t blendTime;
|
||||
float bounds[6];
|
||||
float boundRadius;
|
||||
int16_t nextAnimation;
|
||||
uint16_t aliasNext;
|
||||
};
|
||||
|
||||
// M2 texture definition
|
||||
struct M2TextureDisk {
|
||||
uint32_t type;
|
||||
|
|
@ -210,6 +253,36 @@ struct M2SkinSubmesh {
|
|||
float sortRadius;
|
||||
};
|
||||
|
||||
// Vanilla M2 skin submesh (32 bytes, version < 264 — no sortCenter/sortRadius)
|
||||
struct M2SkinSubmeshVanilla {
|
||||
uint16_t id;
|
||||
uint16_t level;
|
||||
uint16_t vertexStart;
|
||||
uint16_t vertexCount;
|
||||
uint16_t indexStart;
|
||||
uint16_t indexCount;
|
||||
uint16_t boneCount;
|
||||
uint16_t boneStart;
|
||||
uint16_t boneInfluences;
|
||||
uint16_t centerBoneIndex;
|
||||
float centerPosition[3];
|
||||
};
|
||||
|
||||
// Embedded skin profile for vanilla M2 (no 'SKIN' magic, offsets are M2-file-relative)
|
||||
struct M2SkinProfileEmbedded {
|
||||
uint32_t nIndices;
|
||||
uint32_t ofsIndices;
|
||||
uint32_t nTriangles;
|
||||
uint32_t ofsTriangles;
|
||||
uint32_t nVertexProperties;
|
||||
uint32_t ofsVertexProperties;
|
||||
uint32_t nSubmeshes;
|
||||
uint32_t ofsSubmeshes;
|
||||
uint32_t nBatches;
|
||||
uint32_t ofsBatches;
|
||||
uint32_t nBones;
|
||||
};
|
||||
|
||||
// Skin batch structure (24 bytes on disk)
|
||||
struct M2BatchDisk {
|
||||
uint8_t flags;
|
||||
|
|
@ -443,14 +516,23 @@ void parseFBlock(const std::vector<uint8_t>& data, uint32_t offset,
|
|||
M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
||||
M2Model model;
|
||||
|
||||
if (m2Data.size() < sizeof(M2Header)) {
|
||||
// Read header with version-aware field parsing.
|
||||
// Vanilla M2 (version < 264) has 3 extra fields totaling +20 bytes:
|
||||
// +8: playableAnimLookup M2Array (after animationLookup)
|
||||
// +4: ofsViews (after nViews, making it a full M2Array)
|
||||
// +8: unknown extra M2Array (after texReplace, before renderFlags)
|
||||
// Also: vanilla bones are 84 bytes (no boneNameCRC), sequences are 68 bytes.
|
||||
constexpr size_t COMMON_PREFIX_SIZE = 0x2C; // magic through ofsAnimationLookup
|
||||
|
||||
if (m2Data.size() < COMMON_PREFIX_SIZE + 16) { // Need at least some fields after prefix
|
||||
core::Logger::getInstance().error("M2 data too small");
|
||||
return model;
|
||||
}
|
||||
|
||||
// Read header
|
||||
M2Header header;
|
||||
std::memcpy(&header, m2Data.data(), sizeof(M2Header));
|
||||
std::memset(&header, 0, sizeof(header));
|
||||
// Read common prefix (magic through ofsAnimationLookup) — same for all versions
|
||||
std::memcpy(&header, m2Data.data(), COMMON_PREFIX_SIZE);
|
||||
|
||||
// Verify magic
|
||||
if (std::strncmp(header.magic, "MD20", 4) != 0) {
|
||||
|
|
@ -458,6 +540,110 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
return model;
|
||||
}
|
||||
|
||||
uint32_t ofsViews = 0;
|
||||
|
||||
if (header.version < 264) {
|
||||
// Vanilla M2: read remaining header fields using cursor, skipping extra fields
|
||||
size_t c = COMMON_PREFIX_SIZE;
|
||||
|
||||
auto r32 = [&]() -> uint32_t {
|
||||
if (c + 4 > m2Data.size()) return 0;
|
||||
uint32_t v;
|
||||
std::memcpy(&v, m2Data.data() + c, 4);
|
||||
c += 4;
|
||||
return v;
|
||||
};
|
||||
|
||||
// Skip playableAnimLookup M2Array (8 bytes)
|
||||
c += 8;
|
||||
|
||||
// Bones through ofsVertices (same field order as WotLK, just shifted)
|
||||
header.nBones = r32();
|
||||
header.ofsBones = r32();
|
||||
header.nKeyBoneLookup = r32();
|
||||
header.ofsKeyBoneLookup = r32();
|
||||
header.nVertices = r32();
|
||||
header.ofsVertices = r32();
|
||||
|
||||
// nViews + ofsViews (vanilla has both, WotLK has only nViews)
|
||||
header.nViews = r32();
|
||||
ofsViews = r32();
|
||||
|
||||
// nColors through ofsTexReplace
|
||||
header.nColors = r32();
|
||||
header.ofsColors = r32();
|
||||
header.nTextures = r32();
|
||||
header.ofsTextures = r32();
|
||||
header.nTransparency = r32();
|
||||
header.ofsTransparency = r32();
|
||||
header.nUVAnimation = r32();
|
||||
header.ofsUVAnimation = r32();
|
||||
header.nTexReplace = r32();
|
||||
header.ofsTexReplace = r32();
|
||||
|
||||
// Skip unknown extra M2Array (8 bytes)
|
||||
c += 8;
|
||||
|
||||
// nRenderFlags through ofsUVAnimLookup
|
||||
header.nRenderFlags = r32();
|
||||
header.ofsRenderFlags = r32();
|
||||
header.nBoneLookupTable = r32();
|
||||
header.ofsBoneLookupTable = r32();
|
||||
header.nTexLookup = r32();
|
||||
header.ofsTexLookup = r32();
|
||||
header.nTexUnits = r32();
|
||||
header.ofsTexUnits = r32();
|
||||
header.nTransLookup = r32();
|
||||
header.ofsTransLookup = r32();
|
||||
header.nUVAnimLookup = r32();
|
||||
header.ofsUVAnimLookup = r32();
|
||||
|
||||
// Float sections (vertexBox, vertexRadius, boundingBox, boundingRadius)
|
||||
if (c + 56 <= m2Data.size()) {
|
||||
std::memcpy(header.vertexBox, m2Data.data() + c, 24); c += 24;
|
||||
std::memcpy(&header.vertexRadius, m2Data.data() + c, 4); c += 4;
|
||||
std::memcpy(header.boundingBox, m2Data.data() + c, 24); c += 24;
|
||||
std::memcpy(&header.boundingRadius, m2Data.data() + c, 4); c += 4;
|
||||
} else { c += 56; }
|
||||
|
||||
// Remaining M2Array pairs
|
||||
header.nBoundingTriangles = r32();
|
||||
header.ofsBoundingTriangles = r32();
|
||||
header.nBoundingVertices = r32();
|
||||
header.ofsBoundingVertices = r32();
|
||||
header.nBoundingNormals = r32();
|
||||
header.ofsBoundingNormals = r32();
|
||||
header.nAttachments = r32();
|
||||
header.ofsAttachments = r32();
|
||||
header.nAttachmentLookup = r32();
|
||||
header.ofsAttachmentLookup = r32();
|
||||
header.nEvents = r32();
|
||||
header.ofsEvents = r32();
|
||||
header.nLights = r32();
|
||||
header.ofsLights = r32();
|
||||
header.nCameras = r32();
|
||||
header.ofsCameras = r32();
|
||||
header.nCameraLookup = r32();
|
||||
header.ofsCameraLookup = r32();
|
||||
header.nRibbonEmitters = r32();
|
||||
header.ofsRibbonEmitters = r32();
|
||||
header.nParticleEmitters = r32();
|
||||
header.ofsParticleEmitters = r32();
|
||||
|
||||
core::Logger::getInstance().info("Vanilla M2 (version ", header.version,
|
||||
"): nVerts=", header.nVertices, " nViews=", header.nViews,
|
||||
" ofsViews=", ofsViews, " nTex=", header.nTextures);
|
||||
} else {
|
||||
// WotLK: read remaining header with simple memcpy (no extra fields)
|
||||
size_t wotlkSize = sizeof(M2Header) - COMMON_PREFIX_SIZE;
|
||||
if (m2Data.size() < COMMON_PREFIX_SIZE + wotlkSize) {
|
||||
core::Logger::getInstance().error("M2 data too small for WotLK header");
|
||||
return model;
|
||||
}
|
||||
std::memcpy(reinterpret_cast<uint8_t*>(&header) + COMMON_PREFIX_SIZE,
|
||||
m2Data.data() + COMMON_PREFIX_SIZE, wotlkSize);
|
||||
}
|
||||
|
||||
core::Logger::getInstance().debug("Loading M2 model (version ", header.version, ")");
|
||||
|
||||
// Read model name
|
||||
|
|
@ -494,27 +680,51 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
|
||||
// Read animation sequences (needed before bones to know sequence count)
|
||||
if (header.nAnimations > 0 && header.ofsAnimations > 0) {
|
||||
auto diskSeqs = readArray<M2SequenceDisk>(m2Data, header.ofsAnimations, header.nAnimations);
|
||||
model.sequences.reserve(diskSeqs.size());
|
||||
model.sequences.reserve(header.nAnimations);
|
||||
|
||||
for (const auto& ds : diskSeqs) {
|
||||
M2Sequence seq;
|
||||
seq.id = ds.id;
|
||||
seq.variationIndex = ds.variationIndex;
|
||||
seq.duration = ds.duration;
|
||||
seq.movingSpeed = ds.movingSpeed;
|
||||
seq.flags = ds.flags;
|
||||
seq.frequency = ds.frequency;
|
||||
seq.replayMin = ds.replayMin;
|
||||
seq.replayMax = ds.replayMax;
|
||||
seq.blendTime = ds.blendTime;
|
||||
seq.boundMin = glm::vec3(ds.bounds[0], ds.bounds[1], ds.bounds[2]);
|
||||
seq.boundMax = glm::vec3(ds.bounds[3], ds.bounds[4], ds.bounds[5]);
|
||||
seq.boundRadius = ds.boundRadius;
|
||||
seq.nextAnimation = ds.nextAnimation;
|
||||
seq.aliasNext = ds.aliasNext;
|
||||
|
||||
model.sequences.push_back(seq);
|
||||
if (header.version < 264) {
|
||||
// Vanilla: 68-byte sequence struct (has startTimestamp + endTimestamp)
|
||||
auto diskSeqs = readArray<M2SequenceDiskVanilla>(m2Data, header.ofsAnimations, header.nAnimations);
|
||||
for (const auto& ds : diskSeqs) {
|
||||
M2Sequence seq;
|
||||
seq.id = ds.id;
|
||||
seq.variationIndex = ds.variationIndex;
|
||||
seq.duration = (ds.endTimestamp > ds.startTimestamp)
|
||||
? (ds.endTimestamp - ds.startTimestamp) : ds.endTimestamp;
|
||||
seq.movingSpeed = ds.movingSpeed;
|
||||
seq.flags = ds.flags;
|
||||
seq.frequency = ds.frequency;
|
||||
seq.replayMin = ds.replayMin;
|
||||
seq.replayMax = ds.replayMax;
|
||||
seq.blendTime = ds.blendTime;
|
||||
seq.boundMin = glm::vec3(ds.bounds[0], ds.bounds[1], ds.bounds[2]);
|
||||
seq.boundMax = glm::vec3(ds.bounds[3], ds.bounds[4], ds.bounds[5]);
|
||||
seq.boundRadius = ds.boundRadius;
|
||||
seq.nextAnimation = ds.nextAnimation;
|
||||
seq.aliasNext = ds.aliasNext;
|
||||
model.sequences.push_back(seq);
|
||||
}
|
||||
} else {
|
||||
// WotLK: 64-byte sequence struct
|
||||
auto diskSeqs = readArray<M2SequenceDisk>(m2Data, header.ofsAnimations, header.nAnimations);
|
||||
for (const auto& ds : diskSeqs) {
|
||||
M2Sequence seq;
|
||||
seq.id = ds.id;
|
||||
seq.variationIndex = ds.variationIndex;
|
||||
seq.duration = ds.duration;
|
||||
seq.movingSpeed = ds.movingSpeed;
|
||||
seq.flags = ds.flags;
|
||||
seq.frequency = ds.frequency;
|
||||
seq.replayMin = ds.replayMin;
|
||||
seq.replayMax = ds.replayMax;
|
||||
seq.blendTime = ds.blendTime;
|
||||
seq.boundMin = glm::vec3(ds.bounds[0], ds.bounds[1], ds.bounds[2]);
|
||||
seq.boundMax = glm::vec3(ds.bounds[3], ds.bounds[4], ds.bounds[5]);
|
||||
seq.boundRadius = ds.boundRadius;
|
||||
seq.nextAnimation = ds.nextAnimation;
|
||||
seq.aliasNext = ds.aliasNext;
|
||||
model.sequences.push_back(seq);
|
||||
}
|
||||
}
|
||||
|
||||
core::Logger::getInstance().debug(" Animation sequences: ", model.sequences.size());
|
||||
|
|
@ -529,8 +739,8 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
|
||||
// Read bones with full animation track data
|
||||
if (header.nBones > 0 && header.ofsBones > 0) {
|
||||
// Verify we have enough data for the full bone structures
|
||||
uint32_t expectedBoneSize = header.nBones * sizeof(M2BoneDisk);
|
||||
size_t boneStructSize = (header.version < 264) ? sizeof(M2BoneDiskVanilla) : sizeof(M2BoneDisk);
|
||||
uint64_t expectedBoneSize = static_cast<uint64_t>(header.nBones) * boneStructSize;
|
||||
if (header.ofsBones + expectedBoneSize > m2Data.size()) {
|
||||
core::Logger::getInstance().warning("M2 bone data extends beyond file, loading with fallback");
|
||||
}
|
||||
|
|
@ -546,8 +756,8 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
}
|
||||
|
||||
for (uint32_t boneIdx = 0; boneIdx < header.nBones; boneIdx++) {
|
||||
uint32_t boneOffset = header.ofsBones + boneIdx * sizeof(M2BoneDisk);
|
||||
if (boneOffset + sizeof(M2BoneDisk) > m2Data.size()) {
|
||||
uint32_t boneOffset = header.ofsBones + boneIdx * boneStructSize;
|
||||
if (boneOffset + boneStructSize > m2Data.size()) {
|
||||
// Fallback: create identity bone
|
||||
M2Bone bone;
|
||||
bone.keyBoneId = -1;
|
||||
|
|
@ -559,19 +769,46 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
continue;
|
||||
}
|
||||
|
||||
M2BoneDisk db = readValue<M2BoneDisk>(m2Data, boneOffset);
|
||||
|
||||
M2Bone bone;
|
||||
bone.keyBoneId = db.keyBoneId;
|
||||
bone.flags = db.flags;
|
||||
bone.parentBone = db.parentBone;
|
||||
bone.submeshId = db.submeshId;
|
||||
bone.pivot = glm::vec3(db.pivot[0], db.pivot[1], db.pivot[2]);
|
||||
M2TrackDisk translation, rotation, scale;
|
||||
|
||||
// Parse animation tracks (skip sequences with external .anim data)
|
||||
parseAnimTrack(m2Data, db.translation, bone.translation, TrackType::VEC3, seqFlags);
|
||||
parseAnimTrack(m2Data, db.rotation, bone.rotation, TrackType::QUAT_COMPRESSED, seqFlags);
|
||||
parseAnimTrack(m2Data, db.scale, bone.scale, TrackType::VEC3, seqFlags);
|
||||
if (header.version < 264) {
|
||||
// Vanilla: 108-byte bone (no boneNameCRC, 28-byte tracks with ranges)
|
||||
M2BoneDiskVanilla db = readValue<M2BoneDiskVanilla>(m2Data, boneOffset);
|
||||
bone.keyBoneId = db.keyBoneId;
|
||||
bone.flags = db.flags;
|
||||
bone.parentBone = db.parentBone;
|
||||
bone.submeshId = db.submeshId;
|
||||
bone.pivot = glm::vec3(db.pivot[0], db.pivot[1], db.pivot[2]);
|
||||
// Convert vanilla 28-byte tracks to WotLK 20-byte format (drop ranges)
|
||||
translation = {db.translation.interpolationType, db.translation.globalSequence,
|
||||
db.translation.nTimestamps, db.translation.ofsTimestamps,
|
||||
db.translation.nKeys, db.translation.ofsKeys};
|
||||
rotation = {db.rotation.interpolationType, db.rotation.globalSequence,
|
||||
db.rotation.nTimestamps, db.rotation.ofsTimestamps,
|
||||
db.rotation.nKeys, db.rotation.ofsKeys};
|
||||
scale = {db.scale.interpolationType, db.scale.globalSequence,
|
||||
db.scale.nTimestamps, db.scale.ofsTimestamps,
|
||||
db.scale.nKeys, db.scale.ofsKeys};
|
||||
} else {
|
||||
// WotLK: 88-byte bone
|
||||
M2BoneDisk db = readValue<M2BoneDisk>(m2Data, boneOffset);
|
||||
bone.keyBoneId = db.keyBoneId;
|
||||
bone.flags = db.flags;
|
||||
bone.parentBone = db.parentBone;
|
||||
bone.submeshId = db.submeshId;
|
||||
bone.pivot = glm::vec3(db.pivot[0], db.pivot[1], db.pivot[2]);
|
||||
translation = db.translation;
|
||||
rotation = db.rotation;
|
||||
scale = db.scale;
|
||||
}
|
||||
|
||||
// Parse animation tracks (skip for vanilla — flat array format differs from WotLK)
|
||||
if (header.version >= 264) {
|
||||
parseAnimTrack(m2Data, translation, bone.translation, TrackType::VEC3, seqFlags);
|
||||
parseAnimTrack(m2Data, rotation, bone.rotation, TrackType::QUAT_COMPRESSED, seqFlags);
|
||||
parseAnimTrack(m2Data, scale, bone.scale, TrackType::VEC3, seqFlags);
|
||||
}
|
||||
|
||||
if (bone.translation.hasData() || bone.rotation.hasData() || bone.scale.hasData()) {
|
||||
bonesWithKeyframes++;
|
||||
|
|
@ -623,8 +860,8 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
core::Logger::getInstance().debug(" Materials: ", model.materials.size());
|
||||
}
|
||||
|
||||
// Read texture transforms (UV animation data)
|
||||
if (header.nUVAnimation > 0 && header.ofsUVAnimation > 0) {
|
||||
// Read texture transforms (UV animation data) — skip for vanilla (different track format)
|
||||
if (header.nUVAnimation > 0 && header.ofsUVAnimation > 0 && header.version >= 264) {
|
||||
// Build per-sequence flags for skipping external .anim data
|
||||
std::vector<uint32_t> seqFlags;
|
||||
seqFlags.reserve(model.sequences.size());
|
||||
|
|
@ -672,8 +909,10 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
}
|
||||
|
||||
// Parse particle emitters (WotLK M2ParticleOld: 0x1DC = 476 bytes per emitter)
|
||||
// Skip for vanilla — emitter struct size differs
|
||||
static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1DC;
|
||||
if (header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
|
||||
if (header.version >= 264 &&
|
||||
header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
|
||||
header.nParticleEmitters < 256 &&
|
||||
static_cast<size_t>(header.ofsParticleEmitters) +
|
||||
static_cast<size_t>(header.nParticleEmitters) * EMITTER_STRUCT_SIZE <= m2Data.size()) {
|
||||
|
|
@ -758,6 +997,113 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
model.collisionNormals.size(), " normals");
|
||||
}
|
||||
|
||||
// Load embedded skin for vanilla M2 (version < 264)
|
||||
// Vanilla M2 files contain skin profiles directly, no external .skin files.
|
||||
if (header.version < 264 && header.nViews > 0 && ofsViews > 0 &&
|
||||
ofsViews + sizeof(M2SkinProfileEmbedded) <= m2Data.size()) {
|
||||
|
||||
M2SkinProfileEmbedded skinProfile;
|
||||
std::memcpy(&skinProfile, m2Data.data() + ofsViews, sizeof(skinProfile));
|
||||
|
||||
// Read vertex lookup table (maps skin-local indices to global vertex indices)
|
||||
std::vector<uint16_t> vertexLookup;
|
||||
if (skinProfile.nIndices > 0 && skinProfile.ofsIndices > 0) {
|
||||
vertexLookup = readArray<uint16_t>(m2Data, skinProfile.ofsIndices, skinProfile.nIndices);
|
||||
}
|
||||
|
||||
// Read triangle indices (indices into the vertex lookup table)
|
||||
std::vector<uint16_t> triangles;
|
||||
if (skinProfile.nTriangles > 0 && skinProfile.ofsTriangles > 0) {
|
||||
triangles = readArray<uint16_t>(m2Data, skinProfile.ofsTriangles, skinProfile.nTriangles);
|
||||
}
|
||||
|
||||
// Resolve two-level indirection: triangle index -> lookup table -> global vertex
|
||||
model.indices.clear();
|
||||
model.indices.reserve(triangles.size());
|
||||
for (uint16_t triIdx : triangles) {
|
||||
if (triIdx < vertexLookup.size()) {
|
||||
uint16_t globalIdx = vertexLookup[triIdx];
|
||||
if (globalIdx < model.vertices.size()) {
|
||||
model.indices.push_back(globalIdx);
|
||||
} else {
|
||||
model.indices.push_back(0);
|
||||
}
|
||||
} else {
|
||||
model.indices.push_back(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Read submeshes (vanilla: 32 bytes each, no sortCenter/sortRadius)
|
||||
std::vector<M2SkinSubmesh> submeshes;
|
||||
if (skinProfile.nSubmeshes > 0 && skinProfile.ofsSubmeshes > 0) {
|
||||
auto vanillaSubmeshes = readArray<M2SkinSubmeshVanilla>(m2Data,
|
||||
skinProfile.ofsSubmeshes, skinProfile.nSubmeshes);
|
||||
submeshes.reserve(vanillaSubmeshes.size());
|
||||
for (const auto& vs : vanillaSubmeshes) {
|
||||
M2SkinSubmesh sm;
|
||||
sm.id = vs.id;
|
||||
sm.level = vs.level;
|
||||
sm.vertexStart = vs.vertexStart;
|
||||
sm.vertexCount = vs.vertexCount;
|
||||
sm.indexStart = vs.indexStart;
|
||||
sm.indexCount = vs.indexCount;
|
||||
sm.boneCount = vs.boneCount;
|
||||
sm.boneStart = vs.boneStart;
|
||||
sm.boneInfluences = vs.boneInfluences;
|
||||
sm.centerBoneIndex = vs.centerBoneIndex;
|
||||
std::memcpy(sm.centerPosition, vs.centerPosition, 12);
|
||||
std::memset(sm.sortCenterPosition, 0, 12);
|
||||
sm.sortRadius = 0;
|
||||
submeshes.push_back(sm);
|
||||
}
|
||||
}
|
||||
|
||||
// Read batches
|
||||
if (skinProfile.nBatches > 0 && skinProfile.ofsBatches > 0) {
|
||||
auto diskBatches = readArray<M2BatchDisk>(m2Data,
|
||||
skinProfile.ofsBatches, skinProfile.nBatches);
|
||||
model.batches.clear();
|
||||
model.batches.reserve(diskBatches.size());
|
||||
|
||||
for (size_t i = 0; i < diskBatches.size(); i++) {
|
||||
const auto& db = diskBatches[i];
|
||||
M2Batch batch;
|
||||
batch.flags = db.flags;
|
||||
batch.priorityPlane = db.priorityPlane;
|
||||
batch.shader = db.shader;
|
||||
batch.skinSectionIndex = db.skinSectionIndex;
|
||||
batch.colorIndex = db.colorIndex;
|
||||
batch.materialIndex = db.materialIndex;
|
||||
batch.materialLayer = db.materialLayer;
|
||||
batch.textureCount = db.textureCount;
|
||||
batch.textureIndex = db.textureComboIndex;
|
||||
batch.textureUnit = db.textureCoordIndex;
|
||||
batch.transparencyIndex = db.textureWeightIndex;
|
||||
batch.textureAnimIndex = db.textureTransformIndex;
|
||||
|
||||
if (db.skinSectionIndex < submeshes.size()) {
|
||||
const auto& sm = submeshes[db.skinSectionIndex];
|
||||
batch.indexStart = sm.indexStart;
|
||||
batch.indexCount = sm.indexCount;
|
||||
batch.vertexStart = sm.vertexStart;
|
||||
batch.vertexCount = sm.vertexCount;
|
||||
batch.submeshId = sm.id;
|
||||
batch.submeshLevel = sm.level;
|
||||
} else {
|
||||
batch.indexStart = 0;
|
||||
batch.indexCount = model.indices.size();
|
||||
batch.vertexStart = 0;
|
||||
batch.vertexCount = model.vertices.size();
|
||||
}
|
||||
|
||||
model.batches.push_back(batch);
|
||||
}
|
||||
}
|
||||
|
||||
core::Logger::getInstance().info("Vanilla M2: embedded skin loaded — ",
|
||||
model.indices.size(), " indices, ", model.batches.size(), " batches");
|
||||
}
|
||||
|
||||
static int m2LoadLogBudget = 200;
|
||||
if (m2LoadLogBudget-- > 0) {
|
||||
core::Logger::getInstance().debug("M2 model loaded: ", model.name);
|
||||
|
|
|
|||
|
|
@ -220,8 +220,14 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("CharPreview tex lookup: bodySkin=", bodySkinPath_, " faceLower=", faceLowerPath,
|
||||
" faceUpper=", faceUpperPath, " hairScalp=", hairScalpPath,
|
||||
" underwear=", underwearPaths.size());
|
||||
|
||||
// Assign texture filenames on model before GPU upload
|
||||
for (auto& tex : model.textures) {
|
||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||
auto& tex = model.textures[ti];
|
||||
LOG_INFO(" model.textures[", ti, "]: type=", tex.type, " filename=", tex.filename);
|
||||
if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) {
|
||||
tex.filename = bodySkinPath_;
|
||||
} else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) {
|
||||
|
|
@ -250,6 +256,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
LOG_WARNING("CharacterPreview: failed to load model to GPU");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("CharPreview: model loaded to GPU, textureLookup size=", model.textureLookup.size(),
|
||||
" materials=", model.materials.size(), " batches=", model.batches.size());
|
||||
|
||||
// Composite body skin + face + underwear overlays
|
||||
if (!bodySkinPath_.empty()) {
|
||||
|
|
@ -311,8 +319,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
|
||||
// Set default geosets (naked character)
|
||||
std::unordered_set<uint16_t> activeGeosets;
|
||||
// Body parts (group 0: IDs 0-18)
|
||||
for (uint16_t i = 0; i <= 18; i++) {
|
||||
// Body parts (group 0: IDs 0-99, vanilla models use up to 27)
|
||||
for (uint16_t i = 0; i <= 99; i++) {
|
||||
activeGeosets.insert(i);
|
||||
}
|
||||
// Hair style geoset: group 1 = 100 + variation + 1
|
||||
|
|
@ -393,7 +401,7 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
|||
|
||||
// --- Geosets ---
|
||||
std::unordered_set<uint16_t> geosets;
|
||||
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
|
||||
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
|
||||
geosets.insert(static_cast<uint16_t>(100 + hairStyle_ + 1)); // Hair style
|
||||
geosets.insert(static_cast<uint16_t>(200 + facialHair_ + 1)); // Facial hair
|
||||
geosets.insert(701); // Ears
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ void AuthScreen::selectServerProfile(int index) {
|
|||
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
|
||||
if (profiles[i].id == s.expansionId) { expansionIndex = i; break; }
|
||||
}
|
||||
core::Application::getInstance().reloadExpansionData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -311,6 +312,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
expansionIndex = i;
|
||||
registry->setActive(profiles[i].id);
|
||||
core::Application::getInstance().reloadExpansionData();
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,8 +246,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
(previewAppearanceBytes_ != character.appearanceBytes) ||
|
||||
(previewFacialFeatures_ != character.facialFeatures) ||
|
||||
(previewUseFemaleModel_ != character.useFemaleModel) ||
|
||||
(previewEquipHash_ != equipHash) ||
|
||||
(!preview_->isModelLoaded());
|
||||
(previewEquipHash_ != equipHash);
|
||||
|
||||
if (changed) {
|
||||
uint8_t skin = character.appearanceBytes & 0xFF;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue