mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09:33:51 +00:00
Initial commit: wowee native WoW 3.3.5a client
This commit is contained in:
commit
ce6cb8f38e
147 changed files with 32347 additions and 0 deletions
48
src/game/character.cpp
Normal file
48
src/game/character.cpp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#include "game/character.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
const char* getRaceName(Race race) {
|
||||
switch (race) {
|
||||
case Race::HUMAN: return "Human";
|
||||
case Race::ORC: return "Orc";
|
||||
case Race::DWARF: return "Dwarf";
|
||||
case Race::NIGHT_ELF: return "Night Elf";
|
||||
case Race::UNDEAD: return "Undead";
|
||||
case Race::TAUREN: return "Tauren";
|
||||
case Race::GNOME: return "Gnome";
|
||||
case Race::TROLL: return "Troll";
|
||||
case Race::GOBLIN: return "Goblin";
|
||||
case Race::BLOOD_ELF: return "Blood Elf";
|
||||
case Race::DRAENEI: return "Draenei";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* getClassName(Class characterClass) {
|
||||
switch (characterClass) {
|
||||
case Class::WARRIOR: return "Warrior";
|
||||
case Class::PALADIN: return "Paladin";
|
||||
case Class::HUNTER: return "Hunter";
|
||||
case Class::ROGUE: return "Rogue";
|
||||
case Class::PRIEST: return "Priest";
|
||||
case Class::DEATH_KNIGHT: return "Death Knight";
|
||||
case Class::SHAMAN: return "Shaman";
|
||||
case Class::MAGE: return "Mage";
|
||||
case Class::WARLOCK: return "Warlock";
|
||||
case Class::DRUID: return "Druid";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* getGenderName(Gender gender) {
|
||||
switch (gender) {
|
||||
case Gender::MALE: return "Male";
|
||||
case Gender::FEMALE: return "Female";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
37
src/game/entity.cpp
Normal file
37
src/game/entity.cpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#include "game/entity.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
void EntityManager::addEntity(uint64_t guid, std::shared_ptr<Entity> entity) {
|
||||
if (!entity) {
|
||||
LOG_WARNING("Attempted to add null entity with GUID: 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
}
|
||||
|
||||
entities[guid] = entity;
|
||||
|
||||
LOG_DEBUG("Added entity: GUID=0x", std::hex, guid, std::dec,
|
||||
", Type=", static_cast<int>(entity->getType()));
|
||||
}
|
||||
|
||||
void EntityManager::removeEntity(uint64_t guid) {
|
||||
auto it = entities.find(guid);
|
||||
if (it != entities.end()) {
|
||||
LOG_DEBUG("Removed entity: GUID=0x", std::hex, guid, std::dec);
|
||||
entities.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Entity> EntityManager::getEntity(uint64_t guid) const {
|
||||
auto it = entities.find(guid);
|
||||
return (it != entities.end()) ? it->second : nullptr;
|
||||
}
|
||||
|
||||
bool EntityManager::hasEntity(uint64_t guid) const {
|
||||
return entities.find(guid) != entities.end();
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
843
src/game/game_handler.cpp
Normal file
843
src/game/game_handler.cpp
Normal file
|
|
@ -0,0 +1,843 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include "game/opcodes.hpp"
|
||||
#include "network/world_socket.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
GameHandler::GameHandler() {
|
||||
LOG_DEBUG("GameHandler created");
|
||||
}
|
||||
|
||||
GameHandler::~GameHandler() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
bool GameHandler::connect(const std::string& host,
|
||||
uint16_t port,
|
||||
const std::vector<uint8_t>& sessionKey,
|
||||
const std::string& accountName,
|
||||
uint32_t build) {
|
||||
|
||||
if (sessionKey.size() != 40) {
|
||||
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
|
||||
fail("Invalid session key");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" CONNECTING TO WORLD SERVER");
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO("Host: ", host);
|
||||
LOG_INFO("Port: ", port);
|
||||
LOG_INFO("Account: ", accountName);
|
||||
LOG_INFO("Build: ", build);
|
||||
|
||||
// Store authentication data
|
||||
this->sessionKey = sessionKey;
|
||||
this->accountName = accountName;
|
||||
this->build = build;
|
||||
|
||||
// Generate random client seed
|
||||
this->clientSeed = generateClientSeed();
|
||||
LOG_DEBUG("Generated client seed: 0x", std::hex, clientSeed, std::dec);
|
||||
|
||||
// Create world socket
|
||||
socket = std::make_unique<network::WorldSocket>();
|
||||
|
||||
// Set up packet callback
|
||||
socket->setPacketCallback([this](const network::Packet& packet) {
|
||||
network::Packet mutablePacket = packet;
|
||||
handlePacket(mutablePacket);
|
||||
});
|
||||
|
||||
// Connect to world server
|
||||
setState(WorldState::CONNECTING);
|
||||
|
||||
if (!socket->connect(host, port)) {
|
||||
LOG_ERROR("Failed to connect to world server");
|
||||
fail("Connection failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
setState(WorldState::CONNECTED);
|
||||
LOG_INFO("Connected to world server, waiting for SMSG_AUTH_CHALLENGE...");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameHandler::disconnect() {
|
||||
if (socket) {
|
||||
socket->disconnect();
|
||||
socket.reset();
|
||||
}
|
||||
setState(WorldState::DISCONNECTED);
|
||||
LOG_INFO("Disconnected from world server");
|
||||
}
|
||||
|
||||
bool GameHandler::isConnected() const {
|
||||
return socket && socket->isConnected();
|
||||
}
|
||||
|
||||
void GameHandler::update(float deltaTime) {
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update socket (processes incoming data and triggers callbacks)
|
||||
socket->update();
|
||||
|
||||
// Validate target still exists
|
||||
if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) {
|
||||
clearTarget();
|
||||
}
|
||||
|
||||
// Send periodic heartbeat if in world
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
timeSinceLastPing += deltaTime;
|
||||
|
||||
if (timeSinceLastPing >= pingInterval) {
|
||||
sendPing();
|
||||
timeSinceLastPing = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handlePacket(network::Packet& packet) {
|
||||
if (packet.getSize() < 1) {
|
||||
LOG_WARNING("Received empty packet");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t opcode = packet.getOpcode();
|
||||
|
||||
LOG_DEBUG("Received world packet: opcode=0x", std::hex, opcode, std::dec,
|
||||
" size=", packet.getSize(), " bytes");
|
||||
|
||||
// Route packet based on opcode
|
||||
Opcode opcodeEnum = static_cast<Opcode>(opcode);
|
||||
|
||||
switch (opcodeEnum) {
|
||||
case Opcode::SMSG_AUTH_CHALLENGE:
|
||||
if (state == WorldState::CONNECTED) {
|
||||
handleAuthChallenge(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_AUTH_CHALLENGE in state: ", (int)state);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_AUTH_RESPONSE:
|
||||
if (state == WorldState::AUTH_SENT) {
|
||||
handleAuthResponse(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_AUTH_RESPONSE in state: ", (int)state);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_CHAR_ENUM:
|
||||
if (state == WorldState::CHAR_LIST_REQUESTED) {
|
||||
handleCharEnum(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_CHAR_ENUM in state: ", (int)state);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_LOGIN_VERIFY_WORLD:
|
||||
if (state == WorldState::ENTERING_WORLD) {
|
||||
handleLoginVerifyWorld(packet);
|
||||
} else {
|
||||
LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", (int)state);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_ACCOUNT_DATA_TIMES:
|
||||
// Can be received at any time after authentication
|
||||
handleAccountDataTimes(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_MOTD:
|
||||
// Can be received at any time after entering world
|
||||
handleMotd(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_PONG:
|
||||
// Can be received at any time after entering world
|
||||
handlePong(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_UPDATE_OBJECT:
|
||||
// Can be received after entering world
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
handleUpdateObject(packet);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_DESTROY_OBJECT:
|
||||
// Can be received after entering world
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
handleDestroyObject(packet);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_MESSAGECHAT:
|
||||
// Can be received after entering world
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
handleMessageChat(packet);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAuthChallenge(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_AUTH_CHALLENGE");
|
||||
|
||||
AuthChallengeData challenge;
|
||||
if (!AuthChallengeParser::parse(packet, challenge)) {
|
||||
fail("Failed to parse SMSG_AUTH_CHALLENGE");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!challenge.isValid()) {
|
||||
fail("Invalid auth challenge data");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store server seed
|
||||
serverSeed = challenge.serverSeed;
|
||||
LOG_DEBUG("Server seed: 0x", std::hex, serverSeed, std::dec);
|
||||
|
||||
setState(WorldState::CHALLENGE_RECEIVED);
|
||||
|
||||
// Send authentication session
|
||||
sendAuthSession();
|
||||
}
|
||||
|
||||
void GameHandler::sendAuthSession() {
|
||||
LOG_INFO("Sending CMSG_AUTH_SESSION");
|
||||
|
||||
// Build authentication packet
|
||||
auto packet = AuthSessionPacket::build(
|
||||
build,
|
||||
accountName,
|
||||
clientSeed,
|
||||
sessionKey,
|
||||
serverSeed
|
||||
);
|
||||
|
||||
LOG_DEBUG("CMSG_AUTH_SESSION packet size: ", packet.getSize(), " bytes");
|
||||
|
||||
// Send packet (NOT encrypted yet)
|
||||
socket->send(packet);
|
||||
|
||||
// CRITICAL: Initialize encryption AFTER sending AUTH_SESSION
|
||||
// but BEFORE receiving AUTH_RESPONSE
|
||||
LOG_INFO("Initializing RC4 header encryption...");
|
||||
socket->initEncryption(sessionKey);
|
||||
|
||||
setState(WorldState::AUTH_SENT);
|
||||
LOG_INFO("CMSG_AUTH_SESSION sent, encryption initialized, waiting for response...");
|
||||
}
|
||||
|
||||
void GameHandler::handleAuthResponse(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_AUTH_RESPONSE");
|
||||
|
||||
AuthResponseData response;
|
||||
if (!AuthResponseParser::parse(packet, response)) {
|
||||
fail("Failed to parse SMSG_AUTH_RESPONSE");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
std::string reason = std::string("Authentication failed: ") +
|
||||
getAuthResultString(response.result);
|
||||
fail(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
// Authentication successful!
|
||||
setState(WorldState::AUTHENTICATED);
|
||||
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" WORLD AUTHENTICATION SUCCESSFUL!");
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO("Connected to world server");
|
||||
LOG_INFO("Ready for character operations");
|
||||
|
||||
setState(WorldState::READY);
|
||||
|
||||
// Call success callback
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::requestCharacterList() {
|
||||
if (state != WorldState::READY && state != WorldState::AUTHENTICATED) {
|
||||
LOG_WARNING("Cannot request character list in state: ", (int)state);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Requesting character list from server...");
|
||||
|
||||
// Build CMSG_CHAR_ENUM packet (no body, just opcode)
|
||||
auto packet = CharEnumPacket::build();
|
||||
|
||||
// Send packet
|
||||
socket->send(packet);
|
||||
|
||||
setState(WorldState::CHAR_LIST_REQUESTED);
|
||||
LOG_INFO("CMSG_CHAR_ENUM sent, waiting for character list...");
|
||||
}
|
||||
|
||||
void GameHandler::handleCharEnum(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_CHAR_ENUM");
|
||||
|
||||
CharEnumResponse response;
|
||||
if (!CharEnumParser::parse(packet, response)) {
|
||||
fail("Failed to parse SMSG_CHAR_ENUM");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store characters
|
||||
characters = response.characters;
|
||||
|
||||
setState(WorldState::CHAR_LIST_RECEIVED);
|
||||
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" CHARACTER LIST RECEIVED");
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO("Found ", characters.size(), " character(s)");
|
||||
|
||||
if (characters.empty()) {
|
||||
LOG_INFO("No characters on this account");
|
||||
} else {
|
||||
LOG_INFO("Characters:");
|
||||
for (size_t i = 0; i < characters.size(); ++i) {
|
||||
const auto& character = characters[i];
|
||||
LOG_INFO(" [", i + 1, "] ", character.name);
|
||||
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
|
||||
LOG_INFO(" ", getRaceName(character.race), " ",
|
||||
getClassName(character.characterClass));
|
||||
LOG_INFO(" Level ", (int)character.level);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Ready to select character");
|
||||
}
|
||||
|
||||
void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||
if (state != WorldState::CHAR_LIST_RECEIVED) {
|
||||
LOG_WARNING("Cannot select character in state: ", (int)state);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" ENTERING WORLD");
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO("Character GUID: 0x", std::hex, characterGuid, std::dec);
|
||||
|
||||
// Find character name for logging
|
||||
for (const auto& character : characters) {
|
||||
if (character.guid == characterGuid) {
|
||||
LOG_INFO("Character: ", character.name);
|
||||
LOG_INFO("Level ", (int)character.level, " ",
|
||||
getRaceName(character.race), " ",
|
||||
getClassName(character.characterClass));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build CMSG_PLAYER_LOGIN packet
|
||||
auto packet = PlayerLoginPacket::build(characterGuid);
|
||||
|
||||
// Send packet
|
||||
socket->send(packet);
|
||||
|
||||
setState(WorldState::ENTERING_WORLD);
|
||||
LOG_INFO("CMSG_PLAYER_LOGIN sent, entering world...");
|
||||
}
|
||||
|
||||
void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD");
|
||||
|
||||
LoginVerifyWorldData data;
|
||||
if (!LoginVerifyWorldParser::parse(packet, data)) {
|
||||
fail("Failed to parse SMSG_LOGIN_VERIFY_WORLD");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.isValid()) {
|
||||
fail("Invalid world entry data");
|
||||
return;
|
||||
}
|
||||
|
||||
// Successfully entered the world!
|
||||
setState(WorldState::IN_WORLD);
|
||||
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" SUCCESSFULLY ENTERED WORLD!");
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO("Map ID: ", data.mapId);
|
||||
LOG_INFO("Position: (", data.x, ", ", data.y, ", ", data.z, ")");
|
||||
LOG_INFO("Orientation: ", data.orientation, " radians");
|
||||
LOG_INFO("Player is now in the game world");
|
||||
|
||||
// Initialize movement info with world entry position
|
||||
movementInfo.x = data.x;
|
||||
movementInfo.y = data.y;
|
||||
movementInfo.z = data.z;
|
||||
movementInfo.orientation = data.orientation;
|
||||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
movementInfo.time = 0;
|
||||
}
|
||||
|
||||
void GameHandler::handleAccountDataTimes(network::Packet& packet) {
|
||||
LOG_DEBUG("Handling SMSG_ACCOUNT_DATA_TIMES");
|
||||
|
||||
AccountDataTimesData data;
|
||||
if (!AccountDataTimesParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_ACCOUNT_DATA_TIMES");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Account data times received (server time: ", data.serverTime, ")");
|
||||
}
|
||||
|
||||
void GameHandler::handleMotd(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_MOTD");
|
||||
|
||||
MotdData data;
|
||||
if (!MotdParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_MOTD");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" MESSAGE OF THE DAY");
|
||||
LOG_INFO("========================================");
|
||||
for (const auto& line : data.lines) {
|
||||
LOG_INFO(line);
|
||||
}
|
||||
LOG_INFO("========================================");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::sendPing() {
|
||||
if (state != WorldState::IN_WORLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment sequence number
|
||||
pingSequence++;
|
||||
|
||||
LOG_DEBUG("Sending CMSG_PING (heartbeat)");
|
||||
LOG_DEBUG(" Sequence: ", pingSequence);
|
||||
|
||||
// Build and send ping packet
|
||||
auto packet = PingPacket::build(pingSequence, lastLatency);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handlePong(network::Packet& packet) {
|
||||
LOG_DEBUG("Handling SMSG_PONG");
|
||||
|
||||
PongData data;
|
||||
if (!PongParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_PONG");
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify sequence matches
|
||||
if (data.sequence != pingSequence) {
|
||||
LOG_WARNING("SMSG_PONG sequence mismatch: expected ", pingSequence,
|
||||
", got ", data.sequence);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Heartbeat acknowledged (sequence: ", data.sequence, ")");
|
||||
}
|
||||
|
||||
void GameHandler::sendMovement(Opcode opcode) {
|
||||
if (state != WorldState::IN_WORLD) {
|
||||
LOG_WARNING("Cannot send movement in state: ", (int)state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update movement time
|
||||
movementInfo.time = ++movementTime;
|
||||
|
||||
// Update movement flags based on opcode
|
||||
switch (opcode) {
|
||||
case Opcode::CMSG_MOVE_START_FORWARD:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FORWARD);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_START_BACKWARD:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::BACKWARD);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_STOP:
|
||||
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::FORWARD) |
|
||||
static_cast<uint32_t>(MovementFlags::BACKWARD));
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_START_STRAFE_LEFT:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::STRAFE_LEFT);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_START_STRAFE_RIGHT:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_STOP_STRAFE:
|
||||
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
|
||||
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT));
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_JUMP:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FALLING);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_START_TURN_LEFT:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_LEFT);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_START_TURN_RIGHT:
|
||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_RIGHT);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_STOP_TURN:
|
||||
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
|
||||
static_cast<uint32_t>(MovementFlags::TURN_RIGHT));
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_FALL_LAND:
|
||||
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::FALLING);
|
||||
break;
|
||||
case Opcode::CMSG_MOVE_HEARTBEAT:
|
||||
// No flag changes — just sends current position
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
|
||||
static_cast<uint16_t>(opcode), std::dec);
|
||||
|
||||
// Build and send movement packet
|
||||
auto packet = MovementPacket::build(opcode, movementInfo);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::setPosition(float x, float y, float z) {
|
||||
movementInfo.x = x;
|
||||
movementInfo.y = y;
|
||||
movementInfo.z = z;
|
||||
}
|
||||
|
||||
void GameHandler::setOrientation(float orientation) {
|
||||
movementInfo.orientation = orientation;
|
||||
}
|
||||
|
||||
void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_UPDATE_OBJECT");
|
||||
|
||||
UpdateObjectData data;
|
||||
if (!UpdateObjectParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process out-of-range objects first
|
||||
for (uint64_t guid : data.outOfRangeGuids) {
|
||||
if (entityManager.hasEntity(guid)) {
|
||||
LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec);
|
||||
entityManager.removeEntity(guid);
|
||||
}
|
||||
}
|
||||
|
||||
// Process update blocks
|
||||
for (const auto& block : data.blocks) {
|
||||
switch (block.updateType) {
|
||||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2: {
|
||||
// Create new entity
|
||||
std::shared_ptr<Entity> entity;
|
||||
|
||||
switch (block.objectType) {
|
||||
case ObjectType::PLAYER:
|
||||
entity = std::make_shared<Player>(block.guid);
|
||||
LOG_INFO("Created player entity: 0x", std::hex, block.guid, std::dec);
|
||||
break;
|
||||
|
||||
case ObjectType::UNIT:
|
||||
entity = std::make_shared<Unit>(block.guid);
|
||||
LOG_INFO("Created unit entity: 0x", std::hex, block.guid, std::dec);
|
||||
break;
|
||||
|
||||
case ObjectType::GAMEOBJECT:
|
||||
entity = std::make_shared<GameObject>(block.guid);
|
||||
LOG_INFO("Created gameobject entity: 0x", std::hex, block.guid, std::dec);
|
||||
break;
|
||||
|
||||
default:
|
||||
entity = std::make_shared<Entity>(block.guid);
|
||||
entity->setType(block.objectType);
|
||||
LOG_INFO("Created generic entity: 0x", std::hex, block.guid, std::dec,
|
||||
", type=", static_cast<int>(block.objectType));
|
||||
break;
|
||||
}
|
||||
|
||||
// Set position from movement block
|
||||
if (block.hasMovement) {
|
||||
entity->setPosition(block.x, block.y, block.z, block.orientation);
|
||||
LOG_DEBUG(" Position: (", block.x, ", ", block.y, ", ", block.z, ")");
|
||||
}
|
||||
|
||||
// Set fields
|
||||
for (const auto& field : block.fields) {
|
||||
entity->setField(field.first, field.second);
|
||||
}
|
||||
|
||||
// Add to manager
|
||||
entityManager.addEntity(block.guid, entity);
|
||||
break;
|
||||
}
|
||||
|
||||
case UpdateType::VALUES: {
|
||||
// Update existing entity fields
|
||||
auto entity = entityManager.getEntity(block.guid);
|
||||
if (entity) {
|
||||
for (const auto& field : block.fields) {
|
||||
entity->setField(field.first, field.second);
|
||||
}
|
||||
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
|
||||
} else {
|
||||
LOG_WARNING("VALUES update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UpdateType::MOVEMENT: {
|
||||
// Update entity position
|
||||
auto entity = entityManager.getEntity(block.guid);
|
||||
if (entity) {
|
||||
entity->setPosition(block.x, block.y, block.z, block.orientation);
|
||||
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
|
||||
} else {
|
||||
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tabCycleStale = true;
|
||||
LOG_INFO("Entity count: ", entityManager.getEntityCount());
|
||||
}
|
||||
|
||||
void GameHandler::handleDestroyObject(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_DESTROY_OBJECT");
|
||||
|
||||
DestroyObjectData data;
|
||||
if (!DestroyObjectParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_DESTROY_OBJECT");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove entity
|
||||
if (entityManager.hasEntity(data.guid)) {
|
||||
entityManager.removeEntity(data.guid);
|
||||
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
|
||||
" (", (data.isDeath ? "death" : "despawn"), ")");
|
||||
} else {
|
||||
LOG_WARNING("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec);
|
||||
}
|
||||
|
||||
tabCycleStale = true;
|
||||
LOG_INFO("Entity count: ", entityManager.getEntityCount());
|
||||
}
|
||||
|
||||
void GameHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) {
|
||||
if (state != WorldState::IN_WORLD) {
|
||||
LOG_WARNING("Cannot send chat in state: ", (int)state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.empty()) {
|
||||
LOG_WARNING("Cannot send empty chat message");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Sending chat message: [", getChatTypeString(type), "] ", message);
|
||||
|
||||
// Determine language based on character (for now, use COMMON)
|
||||
ChatLanguage language = ChatLanguage::COMMON;
|
||||
|
||||
// Build and send packet
|
||||
auto packet = MessageChatPacket::build(type, language, message, target);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handleMessageChat(network::Packet& packet) {
|
||||
LOG_DEBUG("Handling SMSG_MESSAGECHAT");
|
||||
|
||||
MessageChatData data;
|
||||
if (!MessageChatParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to chat history
|
||||
chatHistory.push_back(data);
|
||||
|
||||
// Limit chat history size
|
||||
if (chatHistory.size() > maxChatHistory) {
|
||||
chatHistory.erase(chatHistory.begin());
|
||||
}
|
||||
|
||||
// Log the message
|
||||
std::string senderInfo;
|
||||
if (!data.senderName.empty()) {
|
||||
senderInfo = data.senderName;
|
||||
} else if (data.senderGuid != 0) {
|
||||
// Try to find entity name
|
||||
auto entity = entityManager.getEntity(data.senderGuid);
|
||||
if (entity && entity->getType() == ObjectType::PLAYER) {
|
||||
auto player = std::dynamic_pointer_cast<Player>(entity);
|
||||
if (player && !player->getName().empty()) {
|
||||
senderInfo = player->getName();
|
||||
} else {
|
||||
senderInfo = "Player-" + std::to_string(data.senderGuid);
|
||||
}
|
||||
} else {
|
||||
senderInfo = "Unknown-" + std::to_string(data.senderGuid);
|
||||
}
|
||||
} else {
|
||||
senderInfo = "System";
|
||||
}
|
||||
|
||||
std::string channelInfo;
|
||||
if (!data.channelName.empty()) {
|
||||
channelInfo = "[" + data.channelName + "] ";
|
||||
}
|
||||
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(" CHAT [", getChatTypeString(data.type), "]");
|
||||
LOG_INFO("========================================");
|
||||
LOG_INFO(channelInfo, senderInfo, ": ", data.message);
|
||||
LOG_INFO("========================================");
|
||||
}
|
||||
|
||||
void GameHandler::setTarget(uint64_t guid) {
|
||||
if (guid == targetGuid) return;
|
||||
targetGuid = guid;
|
||||
if (guid != 0) {
|
||||
LOG_INFO("Target set: 0x", std::hex, guid, std::dec);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::clearTarget() {
|
||||
if (targetGuid != 0) {
|
||||
LOG_INFO("Target cleared");
|
||||
}
|
||||
targetGuid = 0;
|
||||
tabCycleIndex = -1;
|
||||
tabCycleStale = true;
|
||||
}
|
||||
|
||||
std::shared_ptr<Entity> GameHandler::getTarget() const {
|
||||
if (targetGuid == 0) return nullptr;
|
||||
return entityManager.getEntity(targetGuid);
|
||||
}
|
||||
|
||||
void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
||||
// Rebuild cycle list if stale
|
||||
if (tabCycleStale) {
|
||||
tabCycleList.clear();
|
||||
tabCycleIndex = -1;
|
||||
|
||||
struct EntityDist {
|
||||
uint64_t guid;
|
||||
float distance;
|
||||
};
|
||||
std::vector<EntityDist> sortable;
|
||||
|
||||
for (const auto& [guid, entity] : entityManager.getEntities()) {
|
||||
auto t = entity->getType();
|
||||
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
|
||||
float dx = entity->getX() - playerX;
|
||||
float dy = entity->getY() - playerY;
|
||||
float dz = entity->getZ() - playerZ;
|
||||
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
sortable.push_back({guid, dist});
|
||||
}
|
||||
|
||||
std::sort(sortable.begin(), sortable.end(),
|
||||
[](const EntityDist& a, const EntityDist& b) { return a.distance < b.distance; });
|
||||
|
||||
for (const auto& ed : sortable) {
|
||||
tabCycleList.push_back(ed.guid);
|
||||
}
|
||||
tabCycleStale = false;
|
||||
}
|
||||
|
||||
if (tabCycleList.empty()) {
|
||||
clearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
tabCycleIndex = (tabCycleIndex + 1) % static_cast<int>(tabCycleList.size());
|
||||
setTarget(tabCycleList[tabCycleIndex]);
|
||||
}
|
||||
|
||||
void GameHandler::addLocalChatMessage(const MessageChatData& msg) {
|
||||
chatHistory.push_back(msg);
|
||||
if (chatHistory.size() > maxChatHistory) {
|
||||
chatHistory.erase(chatHistory.begin());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<MessageChatData> GameHandler::getChatHistory(size_t maxMessages) const {
|
||||
if (maxMessages == 0 || maxMessages >= chatHistory.size()) {
|
||||
return chatHistory;
|
||||
}
|
||||
|
||||
// Return last N messages
|
||||
return std::vector<MessageChatData>(
|
||||
chatHistory.end() - maxMessages,
|
||||
chatHistory.end()
|
||||
);
|
||||
}
|
||||
|
||||
uint32_t GameHandler::generateClientSeed() {
|
||||
// Generate cryptographically random seed
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<uint32_t> dis(1, 0xFFFFFFFF);
|
||||
return dis(gen);
|
||||
}
|
||||
|
||||
void GameHandler::setState(WorldState newState) {
|
||||
if (state != newState) {
|
||||
LOG_DEBUG("World state: ", (int)state, " -> ", (int)newState);
|
||||
state = newState;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::fail(const std::string& reason) {
|
||||
LOG_ERROR("World connection failed: ", reason);
|
||||
setState(WorldState::FAILED);
|
||||
|
||||
if (onFailure) {
|
||||
onFailure(reason);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
274
src/game/inventory.cpp
Normal file
274
src/game/inventory.cpp
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
#include "game/inventory.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
static const ItemSlot EMPTY_SLOT{};
|
||||
|
||||
Inventory::Inventory() = default;
|
||||
|
||||
const ItemSlot& Inventory::getBackpackSlot(int index) const {
|
||||
if (index < 0 || index >= BACKPACK_SLOTS) return EMPTY_SLOT;
|
||||
return backpack[index];
|
||||
}
|
||||
|
||||
bool Inventory::setBackpackSlot(int index, const ItemDef& item) {
|
||||
if (index < 0 || index >= BACKPACK_SLOTS) return false;
|
||||
backpack[index].item = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inventory::clearBackpackSlot(int index) {
|
||||
if (index < 0 || index >= BACKPACK_SLOTS) return false;
|
||||
backpack[index].item = ItemDef{};
|
||||
return true;
|
||||
}
|
||||
|
||||
const ItemSlot& Inventory::getEquipSlot(EquipSlot slot) const {
|
||||
int idx = static_cast<int>(slot);
|
||||
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return EMPTY_SLOT;
|
||||
return equipment[idx];
|
||||
}
|
||||
|
||||
bool Inventory::setEquipSlot(EquipSlot slot, const ItemDef& item) {
|
||||
int idx = static_cast<int>(slot);
|
||||
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false;
|
||||
equipment[idx].item = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inventory::clearEquipSlot(EquipSlot slot) {
|
||||
int idx = static_cast<int>(slot);
|
||||
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false;
|
||||
equipment[idx].item = ItemDef{};
|
||||
return true;
|
||||
}
|
||||
|
||||
int Inventory::getBagSize(int bagIndex) const {
|
||||
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0;
|
||||
return bags[bagIndex].size;
|
||||
}
|
||||
|
||||
const ItemSlot& Inventory::getBagSlot(int bagIndex, int slotIndex) const {
|
||||
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return EMPTY_SLOT;
|
||||
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return EMPTY_SLOT;
|
||||
return bags[bagIndex].slots[slotIndex];
|
||||
}
|
||||
|
||||
bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) {
|
||||
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false;
|
||||
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false;
|
||||
bags[bagIndex].slots[slotIndex].item = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
int Inventory::findFreeBackpackSlot() const {
|
||||
for (int i = 0; i < BACKPACK_SLOTS; i++) {
|
||||
if (backpack[i].empty()) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool Inventory::addItem(const ItemDef& item) {
|
||||
// Try stacking first
|
||||
if (item.maxStack > 1) {
|
||||
for (int i = 0; i < BACKPACK_SLOTS; i++) {
|
||||
if (!backpack[i].empty() &&
|
||||
backpack[i].item.itemId == item.itemId &&
|
||||
backpack[i].item.stackCount < backpack[i].item.maxStack) {
|
||||
uint32_t space = backpack[i].item.maxStack - backpack[i].item.stackCount;
|
||||
uint32_t toAdd = std::min(space, item.stackCount);
|
||||
backpack[i].item.stackCount += toAdd;
|
||||
if (toAdd >= item.stackCount) return true;
|
||||
// Remaining needs a new slot — fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int slot = findFreeBackpackSlot();
|
||||
if (slot < 0) return false;
|
||||
backpack[slot].item = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Inventory::populateTestItems() {
|
||||
// Equipment
|
||||
{
|
||||
ItemDef sword;
|
||||
sword.itemId = 25;
|
||||
sword.name = "Worn Shortsword";
|
||||
sword.quality = ItemQuality::COMMON;
|
||||
sword.inventoryType = 21; // Main Hand
|
||||
sword.strength = 1;
|
||||
sword.displayInfoId = 1542; // Sword_1H_Short_A_02.m2
|
||||
sword.subclassName = "Sword";
|
||||
setEquipSlot(EquipSlot::MAIN_HAND, sword);
|
||||
}
|
||||
{
|
||||
ItemDef shield;
|
||||
shield.itemId = 2129;
|
||||
shield.name = "Large Round Shield";
|
||||
shield.quality = ItemQuality::COMMON;
|
||||
shield.inventoryType = 14; // Off Hand (Shield)
|
||||
shield.armor = 18;
|
||||
shield.stamina = 1;
|
||||
shield.displayInfoId = 18662; // Shield_Round_A_01.m2
|
||||
shield.subclassName = "Shield";
|
||||
setEquipSlot(EquipSlot::OFF_HAND, shield);
|
||||
}
|
||||
// Shirt/pants/boots in backpack (character model renders bare geosets)
|
||||
{
|
||||
ItemDef shirt;
|
||||
shirt.itemId = 38;
|
||||
shirt.name = "Recruit's Shirt";
|
||||
shirt.quality = ItemQuality::COMMON;
|
||||
shirt.inventoryType = 4; // Shirt
|
||||
shirt.displayInfoId = 2163;
|
||||
addItem(shirt);
|
||||
}
|
||||
{
|
||||
ItemDef pants;
|
||||
pants.itemId = 39;
|
||||
pants.name = "Recruit's Pants";
|
||||
pants.quality = ItemQuality::COMMON;
|
||||
pants.inventoryType = 7; // Legs
|
||||
pants.armor = 4;
|
||||
pants.displayInfoId = 1883;
|
||||
addItem(pants);
|
||||
}
|
||||
{
|
||||
ItemDef boots;
|
||||
boots.itemId = 40;
|
||||
boots.name = "Recruit's Boots";
|
||||
boots.quality = ItemQuality::COMMON;
|
||||
boots.inventoryType = 8; // Feet
|
||||
boots.armor = 3;
|
||||
boots.displayInfoId = 2166;
|
||||
addItem(boots);
|
||||
}
|
||||
|
||||
// Backpack items
|
||||
{
|
||||
ItemDef potion;
|
||||
potion.itemId = 118;
|
||||
potion.name = "Minor Healing Potion";
|
||||
potion.quality = ItemQuality::COMMON;
|
||||
potion.stackCount = 3;
|
||||
potion.maxStack = 5;
|
||||
addItem(potion);
|
||||
}
|
||||
{
|
||||
ItemDef hearthstone;
|
||||
hearthstone.itemId = 6948;
|
||||
hearthstone.name = "Hearthstone";
|
||||
hearthstone.quality = ItemQuality::COMMON;
|
||||
addItem(hearthstone);
|
||||
}
|
||||
{
|
||||
ItemDef leather;
|
||||
leather.itemId = 2318;
|
||||
leather.name = "Light Leather";
|
||||
leather.quality = ItemQuality::COMMON;
|
||||
leather.stackCount = 5;
|
||||
leather.maxStack = 20;
|
||||
addItem(leather);
|
||||
}
|
||||
{
|
||||
ItemDef cloth;
|
||||
cloth.itemId = 2589;
|
||||
cloth.name = "Linen Cloth";
|
||||
cloth.quality = ItemQuality::COMMON;
|
||||
cloth.stackCount = 8;
|
||||
cloth.maxStack = 20;
|
||||
addItem(cloth);
|
||||
}
|
||||
{
|
||||
ItemDef quest;
|
||||
quest.itemId = 50000;
|
||||
quest.name = "Kobold Candle";
|
||||
quest.quality = ItemQuality::COMMON;
|
||||
quest.stackCount = 4;
|
||||
quest.maxStack = 10;
|
||||
addItem(quest);
|
||||
}
|
||||
{
|
||||
ItemDef ring;
|
||||
ring.itemId = 11287;
|
||||
ring.name = "Verdant Ring";
|
||||
ring.quality = ItemQuality::UNCOMMON;
|
||||
ring.inventoryType = 11; // Ring
|
||||
ring.stamina = 3;
|
||||
ring.spirit = 2;
|
||||
addItem(ring);
|
||||
}
|
||||
{
|
||||
ItemDef cloak;
|
||||
cloak.itemId = 2570;
|
||||
cloak.name = "Linen Cloak";
|
||||
cloak.quality = ItemQuality::UNCOMMON;
|
||||
cloak.inventoryType = 16; // Back
|
||||
cloak.armor = 10;
|
||||
cloak.agility = 1;
|
||||
cloak.displayInfoId = 15055;
|
||||
addItem(cloak);
|
||||
}
|
||||
{
|
||||
ItemDef rareAxe;
|
||||
rareAxe.itemId = 15268;
|
||||
rareAxe.name = "Stoneslayer";
|
||||
rareAxe.quality = ItemQuality::RARE;
|
||||
rareAxe.inventoryType = 17; // Two-Hand
|
||||
rareAxe.strength = 8;
|
||||
rareAxe.stamina = 7;
|
||||
rareAxe.subclassName = "Axe";
|
||||
rareAxe.displayInfoId = 782; // Axe_2H_Battle_B_01.m2
|
||||
addItem(rareAxe);
|
||||
}
|
||||
|
||||
LOG_INFO("Inventory: populated test items (2 equipped, 11 backpack)");
|
||||
}
|
||||
|
||||
const char* getQualityName(ItemQuality quality) {
|
||||
switch (quality) {
|
||||
case ItemQuality::POOR: return "Poor";
|
||||
case ItemQuality::COMMON: return "Common";
|
||||
case ItemQuality::UNCOMMON: return "Uncommon";
|
||||
case ItemQuality::RARE: return "Rare";
|
||||
case ItemQuality::EPIC: return "Epic";
|
||||
case ItemQuality::LEGENDARY: return "Legendary";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* getEquipSlotName(EquipSlot slot) {
|
||||
switch (slot) {
|
||||
case EquipSlot::HEAD: return "Head";
|
||||
case EquipSlot::NECK: return "Neck";
|
||||
case EquipSlot::SHOULDERS: return "Shoulders";
|
||||
case EquipSlot::SHIRT: return "Shirt";
|
||||
case EquipSlot::CHEST: return "Chest";
|
||||
case EquipSlot::WAIST: return "Waist";
|
||||
case EquipSlot::LEGS: return "Legs";
|
||||
case EquipSlot::FEET: return "Feet";
|
||||
case EquipSlot::WRISTS: return "Wrists";
|
||||
case EquipSlot::HANDS: return "Hands";
|
||||
case EquipSlot::RING1: return "Ring 1";
|
||||
case EquipSlot::RING2: return "Ring 2";
|
||||
case EquipSlot::TRINKET1: return "Trinket 1";
|
||||
case EquipSlot::TRINKET2: return "Trinket 2";
|
||||
case EquipSlot::BACK: return "Back";
|
||||
case EquipSlot::MAIN_HAND: return "Main Hand";
|
||||
case EquipSlot::OFF_HAND: return "Off Hand";
|
||||
case EquipSlot::RANGED: return "Ranged";
|
||||
case EquipSlot::TABARD: return "Tabard";
|
||||
case EquipSlot::BAG1: return "Bag 1";
|
||||
case EquipSlot::BAG2: return "Bag 2";
|
||||
case EquipSlot::BAG3: return "Bag 3";
|
||||
case EquipSlot::BAG4: return "Bag 4";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
374
src/game/npc_manager.cpp
Normal file
374
src/game/npc_manager.cpp
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
#include "game/npc_manager.hpp"
|
||||
#include "game/entity.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
static constexpr float ZEROPOINT = 32.0f * 533.33333f;
|
||||
|
||||
// Random emote animation IDs (humanoid only)
|
||||
static const uint32_t EMOTE_ANIMS[] = { 60, 66, 67, 70 }; // Talk, Bow, Wave, Laugh
|
||||
static constexpr int NUM_EMOTE_ANIMS = 4;
|
||||
|
||||
static float randomFloat(float lo, float hi) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_real_distribution<float> dist(lo, hi);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
static std::string toLowerStr(const std::string& s) {
|
||||
std::string out = s;
|
||||
for (char& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Look up texture variants for a creature M2 using CreatureDisplayInfo.dbc
|
||||
// Returns up to 3 texture variant names (for type 1, 2, 3 texture slots)
|
||||
static std::vector<std::string> lookupTextureVariants(
|
||||
pipeline::AssetManager* am, const std::string& m2Path) {
|
||||
std::vector<std::string> variants;
|
||||
|
||||
auto modelDataDbc = am->loadDBC("CreatureModelData.dbc");
|
||||
auto displayInfoDbc = am->loadDBC("CreatureDisplayInfo.dbc");
|
||||
if (!modelDataDbc || !displayInfoDbc) return variants;
|
||||
|
||||
// CreatureModelData stores .mdx paths; convert our .m2 path for matching
|
||||
std::string mdxPath = m2Path;
|
||||
if (mdxPath.size() > 3) {
|
||||
mdxPath = mdxPath.substr(0, mdxPath.size() - 3) + ".mdx";
|
||||
}
|
||||
std::string mdxLower = toLowerStr(mdxPath);
|
||||
|
||||
// Find model ID from CreatureModelData (col 0 = ID, col 2 = modelName)
|
||||
uint32_t creatureModelId = 0;
|
||||
for (uint32_t r = 0; r < modelDataDbc->getRecordCount(); r++) {
|
||||
std::string dbcModel = modelDataDbc->getString(r, 2);
|
||||
if (toLowerStr(dbcModel) == mdxLower) {
|
||||
creatureModelId = modelDataDbc->getUInt32(r, 0);
|
||||
LOG_INFO("NpcManager: DBC match for '", m2Path,
|
||||
"' -> CreatureModelData ID ", creatureModelId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (creatureModelId == 0) return variants;
|
||||
|
||||
// Find first CreatureDisplayInfo entry for this model
|
||||
// Col 0=ID, 1=ModelID, 6=TextureVariation_1, 7=TextureVariation_2, 8=TextureVariation_3
|
||||
for (uint32_t r = 0; r < displayInfoDbc->getRecordCount(); r++) {
|
||||
if (displayInfoDbc->getUInt32(r, 1) == creatureModelId) {
|
||||
std::string v1 = displayInfoDbc->getString(r, 6);
|
||||
std::string v2 = displayInfoDbc->getString(r, 7);
|
||||
std::string v3 = displayInfoDbc->getString(r, 8);
|
||||
if (!v1.empty()) variants.push_back(v1);
|
||||
if (!v2.empty()) variants.push_back(v2);
|
||||
if (!v3.empty()) variants.push_back(v3);
|
||||
LOG_INFO("NpcManager: DisplayInfo textures: '", v1, "', '", v2, "', '", v3, "'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return variants;
|
||||
}
|
||||
|
||||
void NpcManager::loadCreatureModel(pipeline::AssetManager* am,
|
||||
rendering::CharacterRenderer* cr,
|
||||
const std::string& m2Path,
|
||||
uint32_t modelId) {
|
||||
auto m2Data = am->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("NpcManager: failed to read M2 file: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
auto model = pipeline::M2Loader::load(m2Data);
|
||||
|
||||
// Derive skin path: replace .m2 with 00.skin
|
||||
std::string skinPath = m2Path;
|
||||
if (skinPath.size() > 3) {
|
||||
skinPath = skinPath.substr(0, skinPath.size() - 3) + "00.skin";
|
||||
}
|
||||
auto skinData = am->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
}
|
||||
|
||||
if (!model.isValid()) {
|
||||
LOG_WARNING("NpcManager: invalid model: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load external .anim files for sequences without flag 0x20
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3); // remove ".m2"
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName),
|
||||
"%s%04u-%02u.anim",
|
||||
basePath.c_str(),
|
||||
model.sequences[si].id,
|
||||
model.sequences[si].variationIndex);
|
||||
auto animFileData = am->readFile(animFileName);
|
||||
if (!animFileData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Resolve creature skin textures ---
|
||||
// Extract model directory: "Creature\Wolf\" from "Creature\Wolf\Wolf.m2"
|
||||
size_t lastSlash = m2Path.find_last_of("\\/");
|
||||
std::string modelDir = (lastSlash != std::string::npos)
|
||||
? m2Path.substr(0, lastSlash + 1) : "";
|
||||
|
||||
// Extract model base name: "Wolf" from "Creature\Wolf\Wolf.m2"
|
||||
std::string modelFileName = (lastSlash != std::string::npos)
|
||||
? m2Path.substr(lastSlash + 1) : m2Path;
|
||||
std::string modelBaseName = modelFileName.substr(0, modelFileName.size() - 3); // remove ".m2"
|
||||
|
||||
// Log existing texture info
|
||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||
LOG_INFO("NpcManager: ", m2Path, " tex[", ti, "] type=",
|
||||
model.textures[ti].type, " file='", model.textures[ti].filename, "'");
|
||||
}
|
||||
|
||||
// Check if any textures need resolution
|
||||
// Type 11 = creature skin 1, type 12 = creature skin 2, type 13 = creature skin 3
|
||||
// Type 1 = character body skin (also possible on some creature models)
|
||||
auto needsResolve = [](uint32_t t) {
|
||||
return t == 11 || t == 12 || t == 13 || t == 1 || t == 2 || t == 3;
|
||||
};
|
||||
|
||||
bool needsVariants = false;
|
||||
for (const auto& tex : model.textures) {
|
||||
if (needsResolve(tex.type) && tex.filename.empty()) {
|
||||
needsVariants = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsVariants) {
|
||||
// Try DBC-based lookup first
|
||||
auto variants = lookupTextureVariants(am, m2Path);
|
||||
|
||||
// Fill in unresolved textures from DBC variants
|
||||
// Creature skin types map: type 11 -> variant[0], type 12 -> variant[1], type 13 -> variant[2]
|
||||
// Also type 1 -> variant[0] as fallback
|
||||
for (auto& tex : model.textures) {
|
||||
if (!needsResolve(tex.type) || !tex.filename.empty()) continue;
|
||||
|
||||
// Determine which variant index this texture type maps to
|
||||
size_t varIdx = 0;
|
||||
if (tex.type == 11 || tex.type == 1) varIdx = 0;
|
||||
else if (tex.type == 12 || tex.type == 2) varIdx = 1;
|
||||
else if (tex.type == 13 || tex.type == 3) varIdx = 2;
|
||||
|
||||
std::string resolved;
|
||||
|
||||
if (varIdx < variants.size() && !variants[varIdx].empty()) {
|
||||
// DBC variant: <ModelDir>\<Variant>.blp
|
||||
resolved = modelDir + variants[varIdx] + ".blp";
|
||||
if (!am->fileExists(resolved)) {
|
||||
LOG_WARNING("NpcManager: DBC texture not found: ", resolved);
|
||||
resolved.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback heuristics if DBC didn't provide a texture
|
||||
if (resolved.empty()) {
|
||||
// Try <ModelDir>\<ModelName>Skin.blp
|
||||
std::string skinTry = modelDir + modelBaseName + "Skin.blp";
|
||||
if (am->fileExists(skinTry)) {
|
||||
resolved = skinTry;
|
||||
} else {
|
||||
// Try <ModelDir>\<ModelName>.blp
|
||||
std::string altTry = modelDir + modelBaseName + ".blp";
|
||||
if (am->fileExists(altTry)) {
|
||||
resolved = altTry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved.empty()) {
|
||||
tex.filename = resolved;
|
||||
LOG_INFO("NpcManager: resolved type-", tex.type,
|
||||
" texture -> '", resolved, "'");
|
||||
} else {
|
||||
LOG_WARNING("NpcManager: could not resolve type-", tex.type,
|
||||
" texture for ", m2Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cr->loadModel(model, modelId);
|
||||
LOG_INFO("NpcManager: loaded model id=", modelId, " path=", m2Path,
|
||||
" verts=", model.vertices.size(), " bones=", model.bones.size(),
|
||||
" anims=", model.sequences.size(), " textures=", model.textures.size());
|
||||
}
|
||||
|
||||
void NpcManager::initialize(pipeline::AssetManager* am,
|
||||
rendering::CharacterRenderer* cr,
|
||||
EntityManager& em,
|
||||
const glm::vec3& playerSpawnGL) {
|
||||
if (!am || !am->isInitialized() || !cr) {
|
||||
LOG_WARNING("NpcManager: cannot initialize — missing AssetManager or CharacterRenderer");
|
||||
return;
|
||||
}
|
||||
|
||||
// Define spawn table: NPC positions are offsets from player spawn in GL coords
|
||||
struct SpawnEntry {
|
||||
const char* name;
|
||||
const char* m2Path;
|
||||
uint32_t level;
|
||||
uint32_t health;
|
||||
float offsetX; // GL X offset from player
|
||||
float offsetY; // GL Y offset from player
|
||||
float rotation;
|
||||
float scale;
|
||||
bool isCritter;
|
||||
};
|
||||
|
||||
static const SpawnEntry spawnTable[] = {
|
||||
// Guards
|
||||
{ "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
||||
60, 42000, -15.0f, 10.0f, 0.0f, 1.0f, false },
|
||||
{ "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
||||
60, 42000, 20.0f, -5.0f, 2.3f, 1.0f, false },
|
||||
{ "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
|
||||
60, 42000, -25.0f, -15.0f, 1.0f, 1.0f, false },
|
||||
|
||||
// Citizens
|
||||
{ "Stormwind Citizen", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
||||
5, 1200, 12.0f, 18.0f, 3.5f, 1.0f, false },
|
||||
{ "Stormwind Citizen", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
|
||||
5, 1200, -8.0f, -22.0f, 5.0f, 1.0f, false },
|
||||
{ "Stormwind Citizen", "Creature\\HumanFemalePeasant\\HumanFemalePeasant.m2",
|
||||
5, 1200, 30.0f, 8.0f, 1.8f, 1.0f, false },
|
||||
{ "Stormwind Citizen", "Creature\\HumanFemalePeasant\\HumanFemalePeasant.m2",
|
||||
5, 1200, -18.0f, 25.0f, 4.2f, 1.0f, false },
|
||||
|
||||
// Critters
|
||||
{ "Wolf", "Creature\\Wolf\\Wolf.m2",
|
||||
1, 42, 35.0f, -20.0f, 0.7f, 1.0f, true },
|
||||
{ "Wolf", "Creature\\Wolf\\Wolf.m2",
|
||||
1, 42, 40.0f, -15.0f, 1.2f, 1.0f, true },
|
||||
{ "Chicken", "Creature\\Chicken\\Chicken.m2",
|
||||
1, 10, -10.0f, 30.0f, 2.0f, 1.0f, true },
|
||||
{ "Chicken", "Creature\\Chicken\\Chicken.m2",
|
||||
1, 10, -12.0f, 33.0f, 3.8f, 1.0f, true },
|
||||
{ "Cat", "Creature\\Cat\\Cat.m2",
|
||||
1, 42, 5.0f, -35.0f, 4.5f, 1.0f, true },
|
||||
{ "Deer", "Creature\\Deer\\Deer.m2",
|
||||
1, 42, -35.0f, -30.0f, 0.3f, 1.0f, true },
|
||||
};
|
||||
|
||||
constexpr size_t spawnCount = sizeof(spawnTable) / sizeof(spawnTable[0]);
|
||||
|
||||
// Load each unique M2 model once
|
||||
for (size_t i = 0; i < spawnCount; i++) {
|
||||
const std::string path(spawnTable[i].m2Path);
|
||||
if (loadedModels.find(path) == loadedModels.end()) {
|
||||
uint32_t mid = nextModelId++;
|
||||
loadCreatureModel(am, cr, path, mid);
|
||||
loadedModels[path] = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn each NPC instance
|
||||
for (size_t i = 0; i < spawnCount; i++) {
|
||||
const auto& s = spawnTable[i];
|
||||
const std::string path(s.m2Path);
|
||||
|
||||
auto it = loadedModels.find(path);
|
||||
if (it == loadedModels.end()) continue; // model failed to load
|
||||
|
||||
uint32_t modelId = it->second;
|
||||
|
||||
// GL position: offset from player spawn
|
||||
glm::vec3 glPos = playerSpawnGL + glm::vec3(s.offsetX, s.offsetY, 0.0f);
|
||||
|
||||
// Create render instance
|
||||
uint32_t instanceId = cr->createInstance(modelId, glPos,
|
||||
glm::vec3(0.0f, 0.0f, s.rotation), s.scale);
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("NpcManager: failed to create instance for ", s.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Play idle animation (anim ID 0)
|
||||
cr->playAnimation(instanceId, 0, true);
|
||||
|
||||
// Assign unique GUID
|
||||
uint64_t guid = nextGuid++;
|
||||
|
||||
// Create entity in EntityManager
|
||||
auto unit = std::make_shared<Unit>(guid);
|
||||
unit->setName(s.name);
|
||||
unit->setLevel(s.level);
|
||||
unit->setHealth(s.health);
|
||||
unit->setMaxHealth(s.health);
|
||||
|
||||
// Convert GL position back to WoW coordinates for targeting system
|
||||
float wowX = ZEROPOINT - glPos.y;
|
||||
float wowY = glPos.z;
|
||||
float wowZ = ZEROPOINT - glPos.x;
|
||||
unit->setPosition(wowX, wowY, wowZ, s.rotation);
|
||||
|
||||
em.addEntity(guid, unit);
|
||||
|
||||
// Track NPC instance
|
||||
NpcInstance npc{};
|
||||
npc.guid = guid;
|
||||
npc.renderInstanceId = instanceId;
|
||||
npc.emoteTimer = randomFloat(5.0f, 15.0f);
|
||||
npc.emoteEndTimer = 0.0f;
|
||||
npc.isEmoting = false;
|
||||
npc.isCritter = s.isCritter;
|
||||
npcs.push_back(npc);
|
||||
|
||||
LOG_INFO("NpcManager: spawned '", s.name, "' guid=0x", std::hex, guid, std::dec,
|
||||
" at GL(", glPos.x, ",", glPos.y, ",", glPos.z, ")");
|
||||
}
|
||||
|
||||
LOG_INFO("NpcManager: initialized ", npcs.size(), " NPCs with ",
|
||||
loadedModels.size(), " unique models");
|
||||
}
|
||||
|
||||
void NpcManager::update(float deltaTime, rendering::CharacterRenderer* cr) {
|
||||
if (!cr) return;
|
||||
|
||||
for (auto& npc : npcs) {
|
||||
// Critters just idle — no emotes
|
||||
if (npc.isCritter) continue;
|
||||
|
||||
if (npc.isEmoting) {
|
||||
npc.emoteEndTimer -= deltaTime;
|
||||
if (npc.emoteEndTimer <= 0.0f) {
|
||||
// Return to idle
|
||||
cr->playAnimation(npc.renderInstanceId, 0, true);
|
||||
npc.isEmoting = false;
|
||||
npc.emoteTimer = randomFloat(5.0f, 15.0f);
|
||||
}
|
||||
} else {
|
||||
npc.emoteTimer -= deltaTime;
|
||||
if (npc.emoteTimer <= 0.0f) {
|
||||
// Play random emote
|
||||
int idx = static_cast<int>(randomFloat(0.0f, static_cast<float>(NUM_EMOTE_ANIMS) - 0.01f));
|
||||
uint32_t emoteAnim = EMOTE_ANIMS[idx];
|
||||
cr->playAnimation(npc.renderInstanceId, emoteAnim, false);
|
||||
npc.isEmoting = true;
|
||||
npc.emoteEndTimer = randomFloat(2.0f, 4.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
7
src/game/opcodes.cpp
Normal file
7
src/game/opcodes.cpp
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "game/opcodes.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
// Opcodes are defined in header
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
7
src/game/player.cpp
Normal file
7
src/game/player.cpp
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "game/player.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
// All methods are inline in header
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
15
src/game/world.cpp
Normal file
15
src/game/world.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#include "game/world.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
void World::update(float deltaTime) {
|
||||
// TODO: Update world state
|
||||
}
|
||||
|
||||
void World::loadMap(uint32_t mapId) {
|
||||
// TODO: Load map data
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
865
src/game/world_packets.cpp
Normal file
865
src/game/world_packets.cpp
Normal file
|
|
@ -0,0 +1,865 @@
|
|||
#include "game/world_packets.hpp"
|
||||
#include "game/opcodes.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
network::Packet AuthSessionPacket::build(uint32_t build,
|
||||
const std::string& accountName,
|
||||
uint32_t clientSeed,
|
||||
const std::vector<uint8_t>& sessionKey,
|
||||
uint32_t serverSeed) {
|
||||
if (sessionKey.size() != 40) {
|
||||
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
|
||||
}
|
||||
|
||||
// Convert account name to uppercase
|
||||
std::string upperAccount = accountName;
|
||||
std::transform(upperAccount.begin(), upperAccount.end(),
|
||||
upperAccount.begin(), ::toupper);
|
||||
|
||||
LOG_INFO("Building CMSG_AUTH_SESSION for account: ", upperAccount);
|
||||
|
||||
// Compute authentication hash
|
||||
auto authHash = computeAuthHash(upperAccount, clientSeed, serverSeed, sessionKey);
|
||||
|
||||
LOG_DEBUG(" Build: ", build);
|
||||
LOG_DEBUG(" Client seed: 0x", std::hex, clientSeed, std::dec);
|
||||
LOG_DEBUG(" Server seed: 0x", std::hex, serverSeed, std::dec);
|
||||
LOG_DEBUG(" Auth hash: ", authHash.size(), " bytes");
|
||||
|
||||
// Create packet (opcode will be added by WorldSocket)
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTH_SESSION));
|
||||
|
||||
// Build number (uint32, little-endian)
|
||||
packet.writeUInt32(build);
|
||||
|
||||
// Unknown uint32 (always 0)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
// Account name (null-terminated string)
|
||||
packet.writeString(upperAccount);
|
||||
|
||||
// Unknown uint32 (always 0)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
// Client seed (uint32, little-endian)
|
||||
packet.writeUInt32(clientSeed);
|
||||
|
||||
// Unknown fields (5x uint32, all zeros)
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
packet.writeUInt32(0);
|
||||
}
|
||||
|
||||
// Authentication hash (20 bytes)
|
||||
packet.writeBytes(authHash.data(), authHash.size());
|
||||
|
||||
// Addon CRC (uint32, can be 0)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes");
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
|
||||
const std::string& accountName,
|
||||
uint32_t clientSeed,
|
||||
uint32_t serverSeed,
|
||||
const std::vector<uint8_t>& sessionKey) {
|
||||
|
||||
// Build hash input:
|
||||
// account_name + [0,0,0,0] + client_seed + server_seed + session_key
|
||||
|
||||
std::vector<uint8_t> hashInput;
|
||||
hashInput.reserve(accountName.size() + 4 + 4 + 4 + sessionKey.size());
|
||||
|
||||
// Account name (as bytes)
|
||||
hashInput.insert(hashInput.end(), accountName.begin(), accountName.end());
|
||||
|
||||
// 4 null bytes
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
hashInput.push_back(0);
|
||||
}
|
||||
|
||||
// Client seed (little-endian)
|
||||
hashInput.push_back(clientSeed & 0xFF);
|
||||
hashInput.push_back((clientSeed >> 8) & 0xFF);
|
||||
hashInput.push_back((clientSeed >> 16) & 0xFF);
|
||||
hashInput.push_back((clientSeed >> 24) & 0xFF);
|
||||
|
||||
// Server seed (little-endian)
|
||||
hashInput.push_back(serverSeed & 0xFF);
|
||||
hashInput.push_back((serverSeed >> 8) & 0xFF);
|
||||
hashInput.push_back((serverSeed >> 16) & 0xFF);
|
||||
hashInput.push_back((serverSeed >> 24) & 0xFF);
|
||||
|
||||
// Session key (40 bytes)
|
||||
hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end());
|
||||
|
||||
LOG_DEBUG("Auth hash input: ", hashInput.size(), " bytes");
|
||||
|
||||
// Compute SHA1 hash
|
||||
return auth::Crypto::sha1(hashInput);
|
||||
}
|
||||
|
||||
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
|
||||
// SMSG_AUTH_CHALLENGE format (WoW 3.3.5a):
|
||||
// uint32 unknown1 (always 1?)
|
||||
// uint32 serverSeed
|
||||
|
||||
if (packet.getSize() < 8) {
|
||||
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.unknown1 = packet.readUInt32();
|
||||
data.serverSeed = packet.readUInt32();
|
||||
|
||||
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE:");
|
||||
LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
|
||||
LOG_INFO(" Server seed: 0x", std::hex, data.serverSeed, std::dec);
|
||||
|
||||
// Note: 3.3.5a has additional data after this (seed2, etc.)
|
||||
// but we only need the first seed for authentication
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuthResponseParser::parse(network::Packet& packet, AuthResponseData& response) {
|
||||
// SMSG_AUTH_RESPONSE format:
|
||||
// uint8 result
|
||||
|
||||
if (packet.getSize() < 1) {
|
||||
LOG_ERROR("SMSG_AUTH_RESPONSE packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t resultCode = packet.readUInt8();
|
||||
response.result = static_cast<AuthResult>(resultCode);
|
||||
|
||||
LOG_INFO("Parsed SMSG_AUTH_RESPONSE: ", getAuthResultString(response.result));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* getAuthResultString(AuthResult result) {
|
||||
switch (result) {
|
||||
case AuthResult::OK:
|
||||
return "OK - Authentication successful";
|
||||
case AuthResult::FAILED:
|
||||
return "FAILED - Authentication failed";
|
||||
case AuthResult::REJECT:
|
||||
return "REJECT - Connection rejected";
|
||||
case AuthResult::BAD_SERVER_PROOF:
|
||||
return "BAD_SERVER_PROOF - Invalid server proof";
|
||||
case AuthResult::UNAVAILABLE:
|
||||
return "UNAVAILABLE - Server unavailable";
|
||||
case AuthResult::SYSTEM_ERROR:
|
||||
return "SYSTEM_ERROR - System error occurred";
|
||||
case AuthResult::BILLING_ERROR:
|
||||
return "BILLING_ERROR - Billing error";
|
||||
case AuthResult::BILLING_EXPIRED:
|
||||
return "BILLING_EXPIRED - Subscription expired";
|
||||
case AuthResult::VERSION_MISMATCH:
|
||||
return "VERSION_MISMATCH - Client version mismatch";
|
||||
case AuthResult::UNKNOWN_ACCOUNT:
|
||||
return "UNKNOWN_ACCOUNT - Account not found";
|
||||
case AuthResult::INCORRECT_PASSWORD:
|
||||
return "INCORRECT_PASSWORD - Wrong password";
|
||||
case AuthResult::SESSION_EXPIRED:
|
||||
return "SESSION_EXPIRED - Session has expired";
|
||||
case AuthResult::SERVER_SHUTTING_DOWN:
|
||||
return "SERVER_SHUTTING_DOWN - Server is shutting down";
|
||||
case AuthResult::ALREADY_LOGGING_IN:
|
||||
return "ALREADY_LOGGING_IN - Already logging in";
|
||||
case AuthResult::LOGIN_SERVER_NOT_FOUND:
|
||||
return "LOGIN_SERVER_NOT_FOUND - Can't contact login server";
|
||||
case AuthResult::WAIT_QUEUE:
|
||||
return "WAIT_QUEUE - Waiting in queue";
|
||||
case AuthResult::BANNED:
|
||||
return "BANNED - Account is banned";
|
||||
case AuthResult::ALREADY_ONLINE:
|
||||
return "ALREADY_ONLINE - Character already logged in";
|
||||
case AuthResult::NO_TIME:
|
||||
return "NO_TIME - No game time remaining";
|
||||
case AuthResult::DB_BUSY:
|
||||
return "DB_BUSY - Database is busy";
|
||||
case AuthResult::SUSPENDED:
|
||||
return "SUSPENDED - Account is suspended";
|
||||
case AuthResult::PARENTAL_CONTROL:
|
||||
return "PARENTAL_CONTROL - Parental controls active";
|
||||
case AuthResult::LOCKED_ENFORCED:
|
||||
return "LOCKED_ENFORCED - Account is locked";
|
||||
default:
|
||||
return "UNKNOWN - Unknown result code";
|
||||
}
|
||||
}
|
||||
|
||||
network::Packet CharEnumPacket::build() {
|
||||
// CMSG_CHAR_ENUM has no body - just the opcode
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
|
||||
|
||||
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) {
|
||||
// Read character count
|
||||
uint8_t count = packet.readUInt8();
|
||||
|
||||
LOG_INFO("Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
|
||||
|
||||
response.characters.clear();
|
||||
response.characters.reserve(count);
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
Character character;
|
||||
|
||||
// Read GUID (8 bytes, little-endian)
|
||||
character.guid = packet.readUInt64();
|
||||
|
||||
// Read name (null-terminated string)
|
||||
character.name = packet.readString();
|
||||
|
||||
// Read race, class, gender
|
||||
character.race = static_cast<Race>(packet.readUInt8());
|
||||
character.characterClass = static_cast<Class>(packet.readUInt8());
|
||||
character.gender = static_cast<Gender>(packet.readUInt8());
|
||||
|
||||
// Read appearance data
|
||||
character.appearanceBytes = packet.readUInt32();
|
||||
character.facialFeatures = packet.readUInt8();
|
||||
|
||||
// Read level
|
||||
character.level = packet.readUInt8();
|
||||
|
||||
// Read location
|
||||
character.zoneId = packet.readUInt32();
|
||||
character.mapId = packet.readUInt32();
|
||||
character.x = packet.readFloat();
|
||||
character.y = packet.readFloat();
|
||||
character.z = packet.readFloat();
|
||||
|
||||
// Read affiliations
|
||||
character.guildId = packet.readUInt32();
|
||||
|
||||
// Read flags
|
||||
character.flags = packet.readUInt32();
|
||||
|
||||
// Skip customization flag (uint32) and unknown byte
|
||||
packet.readUInt32(); // Customization
|
||||
packet.readUInt8(); // Unknown
|
||||
|
||||
// Read pet data (always present, even if no pet)
|
||||
character.pet.displayModel = packet.readUInt32();
|
||||
character.pet.level = packet.readUInt32();
|
||||
character.pet.family = packet.readUInt32();
|
||||
|
||||
// Read equipment (23 items)
|
||||
character.equipment.reserve(23);
|
||||
for (int j = 0; j < 23; ++j) {
|
||||
EquipmentItem item;
|
||||
item.displayModel = packet.readUInt32();
|
||||
item.inventoryType = packet.readUInt8();
|
||||
item.enchantment = packet.readUInt32();
|
||||
character.equipment.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO(" Character ", (int)(i + 1), ": ", character.name);
|
||||
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
|
||||
LOG_INFO(" ", getRaceName(character.race), " ",
|
||||
getClassName(character.characterClass), " (",
|
||||
getGenderName(character.gender), ")");
|
||||
LOG_INFO(" Level: ", (int)character.level);
|
||||
LOG_INFO(" Location: Zone ", character.zoneId, ", Map ", character.mapId);
|
||||
LOG_INFO(" Position: (", character.x, ", ", character.y, ", ", character.z, ")");
|
||||
if (character.hasGuild()) {
|
||||
LOG_INFO(" Guild ID: ", character.guildId);
|
||||
}
|
||||
if (character.hasPet()) {
|
||||
LOG_INFO(" Pet: Model ", character.pet.displayModel,
|
||||
", Level ", character.pet.level);
|
||||
}
|
||||
|
||||
response.characters.push_back(character);
|
||||
}
|
||||
|
||||
LOG_INFO("Successfully parsed ", response.characters.size(), " characters");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PLAYER_LOGIN));
|
||||
|
||||
// Write character GUID (8 bytes, little-endian)
|
||||
packet.writeUInt64(characterGuid);
|
||||
|
||||
LOG_INFO("Built CMSG_PLAYER_LOGIN packet");
|
||||
LOG_INFO(" Character GUID: 0x", std::hex, characterGuid, std::dec);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData& data) {
|
||||
// SMSG_LOGIN_VERIFY_WORLD format (WoW 3.3.5a):
|
||||
// uint32 mapId
|
||||
// float x, y, z (position)
|
||||
// float orientation
|
||||
|
||||
if (packet.getSize() < 20) {
|
||||
LOG_ERROR("SMSG_LOGIN_VERIFY_WORLD packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.mapId = packet.readUInt32();
|
||||
data.x = packet.readFloat();
|
||||
data.y = packet.readFloat();
|
||||
data.z = packet.readFloat();
|
||||
data.orientation = packet.readFloat();
|
||||
|
||||
LOG_INFO("Parsed SMSG_LOGIN_VERIFY_WORLD:");
|
||||
LOG_INFO(" Map ID: ", data.mapId);
|
||||
LOG_INFO(" Position: (", data.x, ", ", data.y, ", ", data.z, ")");
|
||||
LOG_INFO(" Orientation: ", data.orientation, " radians");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData& data) {
|
||||
// SMSG_ACCOUNT_DATA_TIMES format (WoW 3.3.5a):
|
||||
// uint32 serverTime (Unix timestamp)
|
||||
// uint8 unknown (always 1?)
|
||||
// uint32[8] accountDataTimes (timestamps for each data slot)
|
||||
|
||||
if (packet.getSize() < 37) {
|
||||
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.serverTime = packet.readUInt32();
|
||||
data.unknown = packet.readUInt8();
|
||||
|
||||
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
|
||||
LOG_DEBUG(" Server time: ", data.serverTime);
|
||||
LOG_DEBUG(" Unknown: ", (int)data.unknown);
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
data.accountDataTimes[i] = packet.readUInt32();
|
||||
if (data.accountDataTimes[i] != 0) {
|
||||
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MotdParser::parse(network::Packet& packet, MotdData& data) {
|
||||
// SMSG_MOTD format (WoW 3.3.5a):
|
||||
// uint32 lineCount
|
||||
// string[lineCount] lines (null-terminated strings)
|
||||
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("SMSG_MOTD packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t lineCount = packet.readUInt32();
|
||||
|
||||
LOG_INFO("Parsed SMSG_MOTD:");
|
||||
LOG_INFO(" Line count: ", lineCount);
|
||||
|
||||
data.lines.clear();
|
||||
data.lines.reserve(lineCount);
|
||||
|
||||
for (uint32_t i = 0; i < lineCount; ++i) {
|
||||
std::string line = packet.readString();
|
||||
data.lines.push_back(line);
|
||||
LOG_INFO(" [", i + 1, "] ", line);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PING));
|
||||
|
||||
// Write sequence number (uint32, little-endian)
|
||||
packet.writeUInt32(sequence);
|
||||
|
||||
// Write latency (uint32, little-endian, in milliseconds)
|
||||
packet.writeUInt32(latency);
|
||||
|
||||
LOG_DEBUG("Built CMSG_PING packet");
|
||||
LOG_DEBUG(" Sequence: ", sequence);
|
||||
LOG_DEBUG(" Latency: ", latency, " ms");
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool PongParser::parse(network::Packet& packet, PongData& data) {
|
||||
// SMSG_PONG format (WoW 3.3.5a):
|
||||
// uint32 sequence (echoed from CMSG_PING)
|
||||
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("SMSG_PONG packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.sequence = packet.readUInt32();
|
||||
|
||||
LOG_DEBUG("Parsed SMSG_PONG:");
|
||||
LOG_DEBUG(" Sequence: ", data.sequence);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info) {
|
||||
network::Packet packet(static_cast<uint16_t>(opcode));
|
||||
|
||||
// Movement packet format (WoW 3.3.5a):
|
||||
// uint32 flags
|
||||
// uint16 flags2
|
||||
// uint32 time
|
||||
// float x, y, z
|
||||
// float orientation
|
||||
|
||||
// Write movement flags
|
||||
packet.writeUInt32(info.flags);
|
||||
packet.writeUInt16(info.flags2);
|
||||
|
||||
// Write timestamp
|
||||
packet.writeUInt32(info.time);
|
||||
|
||||
// Write position
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.x), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.y), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.z), sizeof(float));
|
||||
|
||||
// Write orientation
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
|
||||
|
||||
// Write pitch if swimming/flying
|
||||
if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
|
||||
}
|
||||
|
||||
// Write fall time if falling
|
||||
if (info.hasFlag(MovementFlags::FALLING)) {
|
||||
packet.writeUInt32(info.fallTime);
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
|
||||
|
||||
// Extended fall data if far falling
|
||||
if (info.hasFlag(MovementFlags::FALLINGFAR)) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("Built movement packet: opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec);
|
||||
LOG_DEBUG(" Flags: 0x", std::hex, info.flags, std::dec);
|
||||
LOG_DEBUG(" Position: (", info.x, ", ", info.y, ", ", info.z, ")");
|
||||
LOG_DEBUG(" Orientation: ", info.orientation);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint64_t UpdateObjectParser::readPackedGuid(network::Packet& packet) {
|
||||
// Read packed GUID format:
|
||||
// First byte is a mask indicating which bytes are present
|
||||
uint8_t mask = packet.readUInt8();
|
||||
|
||||
if (mask == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t guid = 0;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (mask & (1 << i)) {
|
||||
uint8_t byte = packet.readUInt8();
|
||||
guid |= (static_cast<uint64_t>(byte) << (i * 8));
|
||||
}
|
||||
}
|
||||
|
||||
return guid;
|
||||
}
|
||||
|
||||
bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
// Skip movement flags and other movement data for now
|
||||
// This is a simplified implementation
|
||||
|
||||
// Read movement flags (not used yet)
|
||||
/*uint32_t flags =*/ packet.readUInt32();
|
||||
/*uint16_t flags2 =*/ packet.readUInt16();
|
||||
|
||||
// Read timestamp (not used yet)
|
||||
/*uint32_t time =*/ packet.readUInt32();
|
||||
|
||||
// Read position
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
block.orientation = packet.readFloat();
|
||||
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_DEBUG(" Movement: (", block.x, ", ", block.y, ", ", block.z, "), orientation=", block.orientation);
|
||||
|
||||
// TODO: Parse additional movement fields based on flags
|
||||
// For now, we'll skip them to keep this simple
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
|
||||
// Read number of blocks (each block is 32 fields = 32 bits)
|
||||
uint8_t blockCount = packet.readUInt8();
|
||||
|
||||
if (blockCount == 0) {
|
||||
return true; // No fields to update
|
||||
}
|
||||
|
||||
LOG_DEBUG(" Parsing ", (int)blockCount, " field blocks");
|
||||
|
||||
// Read update mask
|
||||
std::vector<uint32_t> updateMask(blockCount);
|
||||
for (int i = 0; i < blockCount; ++i) {
|
||||
updateMask[i] = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Read field values for each bit set in mask
|
||||
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
|
||||
uint32_t mask = updateMask[blockIdx];
|
||||
|
||||
for (int bit = 0; bit < 32; ++bit) {
|
||||
if (mask & (1 << bit)) {
|
||||
uint16_t fieldIndex = blockIdx * 32 + bit;
|
||||
uint32_t value = packet.readUInt32();
|
||||
block.fields[fieldIndex] = value;
|
||||
|
||||
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
// Read update type
|
||||
uint8_t updateTypeVal = packet.readUInt8();
|
||||
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
||||
|
||||
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
|
||||
|
||||
switch (block.updateType) {
|
||||
case UpdateType::VALUES: {
|
||||
// Partial update - changed fields only
|
||||
block.guid = readPackedGuid(packet);
|
||||
LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
return parseUpdateFields(packet, block);
|
||||
}
|
||||
|
||||
case UpdateType::MOVEMENT: {
|
||||
// Movement update
|
||||
block.guid = readPackedGuid(packet);
|
||||
LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
return parseMovementBlock(packet, block);
|
||||
}
|
||||
|
||||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2: {
|
||||
// Create new object with full data
|
||||
block.guid = readPackedGuid(packet);
|
||||
LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
// Read object type
|
||||
uint8_t objectTypeVal = packet.readUInt8();
|
||||
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
||||
LOG_DEBUG(" Object type: ", (int)objectTypeVal);
|
||||
|
||||
// Parse movement if present
|
||||
bool hasMovement = parseMovementBlock(packet, block);
|
||||
if (!hasMovement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse update fields
|
||||
return parseUpdateFields(packet, block);
|
||||
}
|
||||
|
||||
case UpdateType::OUT_OF_RANGE_OBJECTS: {
|
||||
// Objects leaving view range - handled differently
|
||||
LOG_DEBUG(" OUT_OF_RANGE_OBJECTS (skipping in block parser)");
|
||||
return true;
|
||||
}
|
||||
|
||||
case UpdateType::NEAR_OBJECTS: {
|
||||
// Objects entering view range - handled differently
|
||||
LOG_DEBUG(" NEAR_OBJECTS (skipping in block parser)");
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
||||
LOG_INFO("Parsing SMSG_UPDATE_OBJECT");
|
||||
|
||||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
LOG_INFO(" Block count: ", data.blockCount);
|
||||
|
||||
// Check for out-of-range objects first
|
||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||
uint8_t firstByte = packet.readUInt8();
|
||||
|
||||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||
// Read out-of-range GUID count
|
||||
uint32_t count = packet.readUInt32();
|
||||
LOG_INFO(" Out-of-range objects: ", count);
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint64_t guid = readPackedGuid(packet);
|
||||
data.outOfRangeGuids.push_back(guid);
|
||||
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
|
||||
}
|
||||
|
||||
// Done - packet may have more blocks after this
|
||||
// Reset read position to after the first byte if needed
|
||||
} else {
|
||||
// Not out-of-range, rewind
|
||||
packet.setReadPos(packet.getReadPos() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse update blocks
|
||||
data.blocks.reserve(data.blockCount);
|
||||
|
||||
for (uint32_t i = 0; i < data.blockCount; ++i) {
|
||||
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
|
||||
|
||||
UpdateBlock block;
|
||||
if (!parseUpdateBlock(packet, block)) {
|
||||
LOG_ERROR("Failed to parse update block ", i + 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
data.blocks.push_back(block);
|
||||
}
|
||||
|
||||
LOG_INFO("Successfully parsed ", data.blocks.size(), " update blocks");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) {
|
||||
// SMSG_DESTROY_OBJECT format:
|
||||
// uint64 guid
|
||||
// uint8 isDeath (0 = despawn, 1 = death)
|
||||
|
||||
if (packet.getSize() < 9) {
|
||||
LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.guid = packet.readUInt64();
|
||||
data.isDeath = (packet.readUInt8() != 0);
|
||||
|
||||
LOG_INFO("Parsed SMSG_DESTROY_OBJECT:");
|
||||
LOG_INFO(" GUID: 0x", std::hex, data.guid, std::dec);
|
||||
LOG_INFO(" Is death: ", data.isDeath ? "yes" : "no");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet MessageChatPacket::build(ChatType type,
|
||||
ChatLanguage language,
|
||||
const std::string& message,
|
||||
const std::string& target) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_MESSAGECHAT));
|
||||
|
||||
// Write chat type
|
||||
packet.writeUInt32(static_cast<uint32_t>(type));
|
||||
|
||||
// Write language
|
||||
packet.writeUInt32(static_cast<uint32_t>(language));
|
||||
|
||||
// Write target (for whispers) or channel name
|
||||
if (type == ChatType::WHISPER) {
|
||||
packet.writeString(target);
|
||||
} else if (type == ChatType::CHANNEL) {
|
||||
packet.writeString(target); // Channel name
|
||||
}
|
||||
|
||||
// Write message
|
||||
packet.writeString(message);
|
||||
|
||||
LOG_DEBUG("Built CMSG_MESSAGECHAT packet");
|
||||
LOG_DEBUG(" Type: ", static_cast<int>(type));
|
||||
LOG_DEBUG(" Language: ", static_cast<int>(language));
|
||||
LOG_DEBUG(" Message: ", message);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
||||
// SMSG_MESSAGECHAT format (WoW 3.3.5a):
|
||||
// uint8 type
|
||||
// uint32 language
|
||||
// uint64 senderGuid
|
||||
// uint32 unknown (always 0)
|
||||
// [type-specific data]
|
||||
// uint32 messageLength
|
||||
// string message
|
||||
// uint8 chatTag
|
||||
|
||||
if (packet.getSize() < 15) {
|
||||
LOG_ERROR("SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read chat type
|
||||
uint8_t typeVal = packet.readUInt8();
|
||||
data.type = static_cast<ChatType>(typeVal);
|
||||
|
||||
// Read language
|
||||
uint32_t langVal = packet.readUInt32();
|
||||
data.language = static_cast<ChatLanguage>(langVal);
|
||||
|
||||
// Read sender GUID
|
||||
data.senderGuid = packet.readUInt64();
|
||||
|
||||
// Read unknown field
|
||||
packet.readUInt32();
|
||||
|
||||
// Type-specific data
|
||||
switch (data.type) {
|
||||
case ChatType::MONSTER_SAY:
|
||||
case ChatType::MONSTER_YELL:
|
||||
case ChatType::MONSTER_EMOTE: {
|
||||
// Read sender name length + name
|
||||
uint32_t nameLen = packet.readUInt32();
|
||||
if (nameLen > 0 && nameLen < 256) {
|
||||
std::vector<char> nameBuffer(nameLen);
|
||||
for (uint32_t i = 0; i < nameLen; ++i) {
|
||||
nameBuffer[i] = static_cast<char>(packet.readUInt8());
|
||||
}
|
||||
data.senderName = std::string(nameBuffer.begin(), nameBuffer.end());
|
||||
}
|
||||
|
||||
// Read receiver GUID (usually 0 for monsters)
|
||||
data.receiverGuid = packet.readUInt64();
|
||||
break;
|
||||
}
|
||||
|
||||
case ChatType::WHISPER_INFORM: {
|
||||
// Read receiver name
|
||||
data.receiverName = packet.readString();
|
||||
break;
|
||||
}
|
||||
|
||||
case ChatType::CHANNEL: {
|
||||
// Read channel name
|
||||
data.channelName = packet.readString();
|
||||
break;
|
||||
}
|
||||
|
||||
case ChatType::ACHIEVEMENT:
|
||||
case ChatType::GUILD_ACHIEVEMENT: {
|
||||
// Read achievement ID
|
||||
packet.readUInt32();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// No additional data for most types
|
||||
break;
|
||||
}
|
||||
|
||||
// Read message length
|
||||
uint32_t messageLen = packet.readUInt32();
|
||||
|
||||
// Read message
|
||||
if (messageLen > 0 && messageLen < 8192) {
|
||||
std::vector<char> msgBuffer(messageLen);
|
||||
for (uint32_t i = 0; i < messageLen; ++i) {
|
||||
msgBuffer[i] = static_cast<char>(packet.readUInt8());
|
||||
}
|
||||
data.message = std::string(msgBuffer.begin(), msgBuffer.end());
|
||||
}
|
||||
|
||||
// Read chat tag
|
||||
data.chatTag = packet.readUInt8();
|
||||
|
||||
LOG_DEBUG("Parsed SMSG_MESSAGECHAT:");
|
||||
LOG_DEBUG(" Type: ", getChatTypeString(data.type));
|
||||
LOG_DEBUG(" Language: ", static_cast<int>(data.language));
|
||||
LOG_DEBUG(" Sender GUID: 0x", std::hex, data.senderGuid, std::dec);
|
||||
if (!data.senderName.empty()) {
|
||||
LOG_DEBUG(" Sender name: ", data.senderName);
|
||||
}
|
||||
if (!data.channelName.empty()) {
|
||||
LOG_DEBUG(" Channel: ", data.channelName);
|
||||
}
|
||||
LOG_DEBUG(" Message: ", data.message);
|
||||
LOG_DEBUG(" Chat tag: 0x", std::hex, (int)data.chatTag, std::dec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* getChatTypeString(ChatType type) {
|
||||
switch (type) {
|
||||
case ChatType::SAY: return "SAY";
|
||||
case ChatType::PARTY: return "PARTY";
|
||||
case ChatType::RAID: return "RAID";
|
||||
case ChatType::GUILD: return "GUILD";
|
||||
case ChatType::OFFICER: return "OFFICER";
|
||||
case ChatType::YELL: return "YELL";
|
||||
case ChatType::WHISPER: return "WHISPER";
|
||||
case ChatType::WHISPER_INFORM: return "WHISPER_INFORM";
|
||||
case ChatType::EMOTE: return "EMOTE";
|
||||
case ChatType::TEXT_EMOTE: return "TEXT_EMOTE";
|
||||
case ChatType::SYSTEM: return "SYSTEM";
|
||||
case ChatType::MONSTER_SAY: return "MONSTER_SAY";
|
||||
case ChatType::MONSTER_YELL: return "MONSTER_YELL";
|
||||
case ChatType::MONSTER_EMOTE: return "MONSTER_EMOTE";
|
||||
case ChatType::CHANNEL: return "CHANNEL";
|
||||
case ChatType::CHANNEL_JOIN: return "CHANNEL_JOIN";
|
||||
case ChatType::CHANNEL_LEAVE: return "CHANNEL_LEAVE";
|
||||
case ChatType::CHANNEL_LIST: return "CHANNEL_LIST";
|
||||
case ChatType::CHANNEL_NOTICE: return "CHANNEL_NOTICE";
|
||||
case ChatType::CHANNEL_NOTICE_USER: return "CHANNEL_NOTICE_USER";
|
||||
case ChatType::AFK: return "AFK";
|
||||
case ChatType::DND: return "DND";
|
||||
case ChatType::IGNORED: return "IGNORED";
|
||||
case ChatType::SKILL: return "SKILL";
|
||||
case ChatType::LOOT: return "LOOT";
|
||||
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
|
||||
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
|
||||
case ChatType::RAID_LEADER: return "RAID_LEADER";
|
||||
case ChatType::RAID_WARNING: return "RAID_WARNING";
|
||||
case ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
|
||||
case ChatType::GUILD_ACHIEVEMENT: return "GUILD_ACHIEVEMENT";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
116
src/game/zone_manager.cpp
Normal file
116
src/game/zone_manager.cpp
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#include "game/zone_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
void ZoneManager::initialize() {
|
||||
// Elwynn Forest (zone 12)
|
||||
ZoneInfo elwynn;
|
||||
elwynn.id = 12;
|
||||
elwynn.name = "Elwynn Forest";
|
||||
elwynn.musicPaths = {
|
||||
"Sound\\Music\\ZoneMusic\\Forest\\DayForest01.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\Forest\\DayForest02.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\Forest\\DayForest03.mp3",
|
||||
};
|
||||
zones[12] = elwynn;
|
||||
|
||||
// Stormwind City (zone 1519)
|
||||
ZoneInfo stormwind;
|
||||
stormwind.id = 1519;
|
||||
stormwind.name = "Stormwind City";
|
||||
stormwind.musicPaths = {
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind04-zone.mp3",
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind05-zone.mp3",
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind06-zone.mp3",
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind07-zone.mp3",
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind08-zone.mp3",
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind09-zone.mp3",
|
||||
"Sound\\Music\\CityMusic\\Stormwind\\stormwind10-zone.mp3",
|
||||
};
|
||||
zones[1519] = stormwind;
|
||||
|
||||
// Dun Morogh (zone 1) - neighboring zone
|
||||
ZoneInfo dunmorogh;
|
||||
dunmorogh.id = 1;
|
||||
dunmorogh.name = "Dun Morogh";
|
||||
dunmorogh.musicPaths = {
|
||||
"Sound\\Music\\ZoneMusic\\Mountain\\DayMountain01.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\Mountain\\DayMountain02.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\Mountain\\DayMountain03.mp3",
|
||||
};
|
||||
zones[1] = dunmorogh;
|
||||
|
||||
// Westfall (zone 40)
|
||||
ZoneInfo westfall;
|
||||
westfall.id = 40;
|
||||
westfall.name = "Westfall";
|
||||
westfall.musicPaths = {
|
||||
"Sound\\Music\\ZoneMusic\\Plains\\DayPlains01.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\Plains\\DayPlains02.mp3",
|
||||
"Sound\\Music\\ZoneMusic\\Plains\\DayPlains03.mp3",
|
||||
};
|
||||
zones[40] = westfall;
|
||||
|
||||
// Tile-to-zone mappings for Azeroth (Eastern Kingdoms)
|
||||
// Elwynn Forest tiles
|
||||
for (int tx = 31; tx <= 34; tx++) {
|
||||
for (int ty = 48; ty <= 51; ty++) {
|
||||
tileToZone[tx * 100 + ty] = 12; // Elwynn
|
||||
}
|
||||
}
|
||||
|
||||
// Stormwind City tiles (northern part of Elwynn area)
|
||||
tileToZone[31 * 100 + 47] = 1519;
|
||||
tileToZone[32 * 100 + 47] = 1519;
|
||||
tileToZone[33 * 100 + 47] = 1519;
|
||||
|
||||
// Westfall tiles (west of Elwynn)
|
||||
for (int ty = 48; ty <= 51; ty++) {
|
||||
tileToZone[35 * 100 + ty] = 40;
|
||||
tileToZone[36 * 100 + ty] = 40;
|
||||
}
|
||||
|
||||
// Dun Morogh tiles (south/east of Elwynn)
|
||||
for (int tx = 31; tx <= 34; tx++) {
|
||||
tileToZone[tx * 100 + 52] = 1;
|
||||
tileToZone[tx * 100 + 53] = 1;
|
||||
}
|
||||
|
||||
std::srand(static_cast<unsigned>(std::time(nullptr)));
|
||||
|
||||
LOG_INFO("Zone manager initialized: ", zones.size(), " zones, ", tileToZone.size(), " tile mappings");
|
||||
}
|
||||
|
||||
uint32_t ZoneManager::getZoneId(int tileX, int tileY) const {
|
||||
int key = tileX * 100 + tileY;
|
||||
auto it = tileToZone.find(key);
|
||||
if (it != tileToZone.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return 0; // Unknown zone
|
||||
}
|
||||
|
||||
const ZoneInfo* ZoneManager::getZoneInfo(uint32_t zoneId) const {
|
||||
auto it = zones.find(zoneId);
|
||||
if (it != zones.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string ZoneManager::getRandomMusic(uint32_t zoneId) const {
|
||||
auto it = zones.find(zoneId);
|
||||
if (it == zones.end() || it->second.musicPaths.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto& paths = it->second.musicPaths;
|
||||
return paths[std::rand() % paths.size()];
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue