Initial commit: wowee native WoW 3.3.5a client

This commit is contained in:
Kelsi 2026-02-02 12:24:50 -08:00
commit ce6cb8f38e
147 changed files with 32347 additions and 0 deletions

48
src/game/character.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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