mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining full 3.3.5a private server compatibility: - Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature name queries, CMSG_SET_ACTIVE_MOVER after login - Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power tracking from UPDATE_OBJECT fields, floating combat text - Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar, cooldown tracking, aura/buff system with cancellation - Phase 4: Group invite/accept/decline/leave, party frames UI, /invite chat command - Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface Also: disable debug HUD/panels by default, gate 3D rendering to IN_GAME state only, fix window resize not updating UI positions.
This commit is contained in:
parent
6bf3fa4ed4
commit
c49bb58e47
14 changed files with 3039 additions and 84 deletions
|
|
@ -106,6 +106,37 @@ void GameHandler::update(float deltaTime) {
|
|||
sendPing();
|
||||
timeSinceLastPing = 0.0f;
|
||||
}
|
||||
|
||||
// Update cast timer (Phase 3)
|
||||
if (casting && castTimeRemaining > 0.0f) {
|
||||
castTimeRemaining -= deltaTime;
|
||||
if (castTimeRemaining <= 0.0f) {
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Update spell cooldowns (Phase 3)
|
||||
for (auto it = spellCooldowns.begin(); it != spellCooldowns.end(); ) {
|
||||
it->second -= deltaTime;
|
||||
if (it->second <= 0.0f) {
|
||||
it = spellCooldowns.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Update action bar cooldowns
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.cooldownRemaining > 0.0f) {
|
||||
slot.cooldownRemaining -= deltaTime;
|
||||
if (slot.cooldownRemaining < 0.0f) slot.cooldownRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Update combat text (Phase 2)
|
||||
updateCombatText(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +223,127 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
|
||||
// ---- Phase 1: Foundation ----
|
||||
case Opcode::SMSG_NAME_QUERY_RESPONSE:
|
||||
handleNameQueryResponse(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_CREATURE_QUERY_RESPONSE:
|
||||
handleCreatureQueryResponse(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
case Opcode::SMSG_ATTACKSTART:
|
||||
handleAttackStart(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ATTACKSTOP:
|
||||
handleAttackStop(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ATTACKERSTATEUPDATE:
|
||||
handleAttackerStateUpdate(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELLNONMELEEDAMAGELOG:
|
||||
handleSpellDamageLog(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELLHEALLOG:
|
||||
handleSpellHealLog(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 3: Spells ----
|
||||
case Opcode::SMSG_INITIAL_SPELLS:
|
||||
handleInitialSpells(packet);
|
||||
break;
|
||||
case Opcode::SMSG_CAST_FAILED:
|
||||
handleCastFailed(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_START:
|
||||
handleSpellStart(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_GO:
|
||||
handleSpellGo(packet);
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_FAILURE:
|
||||
// Spell failed mid-cast
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
break;
|
||||
case Opcode::SMSG_SPELL_COOLDOWN:
|
||||
handleSpellCooldown(packet);
|
||||
break;
|
||||
case Opcode::SMSG_COOLDOWN_EVENT:
|
||||
handleCooldownEvent(packet);
|
||||
break;
|
||||
case Opcode::SMSG_AURA_UPDATE:
|
||||
handleAuraUpdate(packet, false);
|
||||
break;
|
||||
case Opcode::SMSG_AURA_UPDATE_ALL:
|
||||
handleAuraUpdate(packet, true);
|
||||
break;
|
||||
case Opcode::SMSG_LEARNED_SPELL:
|
||||
handleLearnedSpell(packet);
|
||||
break;
|
||||
case Opcode::SMSG_REMOVED_SPELL:
|
||||
handleRemovedSpell(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 4: Group ----
|
||||
case Opcode::SMSG_GROUP_INVITE:
|
||||
handleGroupInvite(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GROUP_DECLINE:
|
||||
handleGroupDecline(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GROUP_LIST:
|
||||
handleGroupList(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GROUP_UNINVITE:
|
||||
handleGroupUninvite(packet);
|
||||
break;
|
||||
case Opcode::SMSG_PARTY_COMMAND_RESULT:
|
||||
handlePartyCommandResult(packet);
|
||||
break;
|
||||
|
||||
// ---- Phase 5: Loot/Gossip/Vendor ----
|
||||
case Opcode::SMSG_LOOT_RESPONSE:
|
||||
handleLootResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LOOT_RELEASE_RESPONSE:
|
||||
handleLootReleaseResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LOOT_REMOVED:
|
||||
handleLootRemoved(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GOSSIP_MESSAGE:
|
||||
handleGossipMessage(packet);
|
||||
break;
|
||||
case Opcode::SMSG_GOSSIP_COMPLETE:
|
||||
handleGossipComplete(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LIST_INVENTORY:
|
||||
handleListInventory(packet);
|
||||
break;
|
||||
|
||||
// Silently ignore common packets we don't handle yet
|
||||
case Opcode::SMSG_FEATURE_SYSTEM_STATUS:
|
||||
case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER:
|
||||
case Opcode::SMSG_SET_PCT_SPELL_MODIFIER:
|
||||
case Opcode::SMSG_SPELL_DELAYED:
|
||||
case Opcode::SMSG_UPDATE_AURA_DURATION:
|
||||
case Opcode::SMSG_PERIODICAURALOG:
|
||||
case Opcode::SMSG_SPELLENERGIZELOG:
|
||||
case Opcode::SMSG_ENVIRONMENTALDAMAGELOG:
|
||||
case Opcode::SMSG_LOOT_MONEY_NOTIFY:
|
||||
case Opcode::SMSG_LOOT_CLEAR_MONEY:
|
||||
case Opcode::SMSG_NPC_TEXT_UPDATE:
|
||||
case Opcode::SMSG_SELL_ITEM:
|
||||
case Opcode::SMSG_BUY_FAILED:
|
||||
case Opcode::SMSG_INVENTORY_CHANGE_FAILURE:
|
||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||
case Opcode::SMSG_GROUP_SET_LEADER:
|
||||
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
|
||||
break;
|
||||
|
|
@ -357,6 +509,9 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
}
|
||||
}
|
||||
|
||||
// Store player GUID
|
||||
playerGuid = characterGuid;
|
||||
|
||||
// Build CMSG_PLAYER_LOGIN packet
|
||||
auto packet = PlayerLoginPacket::build(characterGuid);
|
||||
|
||||
|
|
@ -400,6 +555,13 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
movementInfo.flags = 0;
|
||||
movementInfo.flags2 = 0;
|
||||
movementInfo.time = 0;
|
||||
|
||||
// Send CMSG_SET_ACTIVE_MOVER (required by some servers)
|
||||
if (playerGuid != 0 && socket) {
|
||||
auto activeMoverPacket = SetActiveMoverPacket::build(playerGuid);
|
||||
socket->send(activeMoverPacket);
|
||||
LOG_INFO("Sent CMSG_SET_ACTIVE_MOVER for player 0x", std::hex, playerGuid, std::dec);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAccountDataTimes(network::Packet& packet) {
|
||||
|
|
@ -603,6 +765,35 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Add to manager
|
||||
entityManager.addEntity(block.guid, entity);
|
||||
|
||||
// Auto-query names (Phase 1)
|
||||
if (block.objectType == ObjectType::PLAYER) {
|
||||
queryPlayerName(block.guid);
|
||||
} else if (block.objectType == ObjectType::UNIT) {
|
||||
// Extract creature entry from fields (UNIT_FIELD_ENTRY = index 54 in 3.3.5a,
|
||||
// but the OBJECT_FIELD_ENTRY is at index 3)
|
||||
auto it = block.fields.find(3); // OBJECT_FIELD_ENTRY
|
||||
if (it != block.fields.end() && it->second != 0) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
unit->setEntry(it->second);
|
||||
queryCreatureInfo(it->second, block.guid);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract health/mana/power from fields (Phase 2)
|
||||
if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
auto hpIt = block.fields.find(24); // UNIT_FIELD_HEALTH
|
||||
if (hpIt != block.fields.end()) unit->setHealth(hpIt->second);
|
||||
auto maxHpIt = block.fields.find(32); // UNIT_FIELD_MAXHEALTH
|
||||
if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second);
|
||||
auto powerIt = block.fields.find(25); // UNIT_FIELD_POWER1
|
||||
if (powerIt != block.fields.end()) unit->setPower(powerIt->second);
|
||||
auto maxPowerIt = block.fields.find(33); // UNIT_FIELD_MAXPOWER1
|
||||
if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second);
|
||||
auto levelIt = block.fields.find(54); // UNIT_FIELD_LEVEL
|
||||
if (levelIt != block.fields.end()) unit->setLevel(levelIt->second);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -613,6 +804,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
for (const auto& field : block.fields) {
|
||||
entity->setField(field.first, field.second);
|
||||
}
|
||||
|
||||
// Update cached health/mana/power values (Phase 2)
|
||||
if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
auto hpIt = block.fields.find(24);
|
||||
if (hpIt != block.fields.end()) unit->setHealth(hpIt->second);
|
||||
auto maxHpIt = block.fields.find(32);
|
||||
if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second);
|
||||
auto powerIt = block.fields.find(25);
|
||||
if (powerIt != block.fields.end()) unit->setPower(powerIt->second);
|
||||
auto maxPowerIt = block.fields.find(33);
|
||||
if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second);
|
||||
auto levelIt = block.fields.find(54);
|
||||
if (levelIt != block.fields.end()) unit->setLevel(levelIt->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);
|
||||
|
|
@ -737,6 +944,13 @@ void GameHandler::handleMessageChat(network::Packet& packet) {
|
|||
void GameHandler::setTarget(uint64_t guid) {
|
||||
if (guid == targetGuid) return;
|
||||
targetGuid = guid;
|
||||
|
||||
// Inform server of target selection (Phase 1)
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = SetSelectionPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
if (guid != 0) {
|
||||
LOG_INFO("Target set: 0x", std::hex, guid, std::dec);
|
||||
}
|
||||
|
|
@ -815,6 +1029,539 @@ std::vector<MessageChatData> GameHandler::getChatHistory(size_t maxMessages) con
|
|||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Name Queries
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::queryPlayerName(uint64_t guid) {
|
||||
if (playerNameCache.count(guid) || pendingNameQueries.count(guid)) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
pendingNameQueries.insert(guid);
|
||||
auto packet = NameQueryPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) {
|
||||
if (creatureInfoCache.count(entry) || pendingCreatureQueries.count(entry)) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
pendingCreatureQueries.insert(entry);
|
||||
auto packet = CreatureQueryPacket::build(entry, guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
std::string GameHandler::getCachedPlayerName(uint64_t guid) const {
|
||||
auto it = playerNameCache.find(guid);
|
||||
return (it != playerNameCache.end()) ? it->second : "";
|
||||
}
|
||||
|
||||
std::string GameHandler::getCachedCreatureName(uint32_t entry) const {
|
||||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? it->second.name : "";
|
||||
}
|
||||
|
||||
void GameHandler::handleNameQueryResponse(network::Packet& packet) {
|
||||
NameQueryResponseData data;
|
||||
if (!NameQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingNameQueries.erase(data.guid);
|
||||
|
||||
if (data.isValid()) {
|
||||
playerNameCache[data.guid] = data.name;
|
||||
// Update entity name
|
||||
auto entity = entityManager.getEntity(data.guid);
|
||||
if (entity && entity->getType() == ObjectType::PLAYER) {
|
||||
auto player = std::static_pointer_cast<Player>(entity);
|
||||
player->setName(data.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
|
||||
CreatureQueryResponseData data;
|
||||
if (!CreatureQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingCreatureQueries.erase(data.entry);
|
||||
|
||||
if (data.isValid()) {
|
||||
creatureInfoCache[data.entry] = data;
|
||||
// Update all unit entities with this entry
|
||||
for (auto& [guid, entity] : entityManager.getEntities()) {
|
||||
if (entity->getType() == ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
if (unit->getEntry() == data.entry) {
|
||||
unit->setName(data.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::startAutoAttack(uint64_t targetGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
autoAttacking = true;
|
||||
autoAttackTarget = targetGuid;
|
||||
auto packet = AttackSwingPacket::build(targetGuid);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec);
|
||||
}
|
||||
|
||||
void GameHandler::stopAutoAttack() {
|
||||
if (!autoAttacking) return;
|
||||
autoAttacking = false;
|
||||
autoAttackTarget = 0;
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = AttackStopPacket::build();
|
||||
socket->send(packet);
|
||||
}
|
||||
LOG_INFO("Stopping auto-attack");
|
||||
}
|
||||
|
||||
void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource) {
|
||||
CombatTextEntry entry;
|
||||
entry.type = type;
|
||||
entry.amount = amount;
|
||||
entry.spellId = spellId;
|
||||
entry.age = 0.0f;
|
||||
entry.isPlayerSource = isPlayerSource;
|
||||
combatText.push_back(entry);
|
||||
}
|
||||
|
||||
void GameHandler::updateCombatText(float deltaTime) {
|
||||
for (auto& entry : combatText) {
|
||||
entry.age += deltaTime;
|
||||
}
|
||||
combatText.erase(
|
||||
std::remove_if(combatText.begin(), combatText.end(),
|
||||
[](const CombatTextEntry& e) { return e.isExpired(); }),
|
||||
combatText.end());
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackStart(network::Packet& packet) {
|
||||
AttackStartData data;
|
||||
if (!AttackStartParser::parse(packet, data)) return;
|
||||
|
||||
if (data.attackerGuid == playerGuid) {
|
||||
autoAttacking = true;
|
||||
autoAttackTarget = data.victimGuid;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackStop(network::Packet& packet) {
|
||||
AttackStopData data;
|
||||
if (!AttackStopParser::parse(packet, data)) return;
|
||||
|
||||
if (data.attackerGuid == playerGuid) {
|
||||
autoAttacking = false;
|
||||
autoAttackTarget = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||
AttackerStateUpdateData data;
|
||||
if (!AttackerStateUpdateParser::parse(packet, data)) return;
|
||||
|
||||
bool isPlayerAttacker = (data.attackerGuid == playerGuid);
|
||||
bool isPlayerTarget = (data.targetGuid == playerGuid);
|
||||
|
||||
if (data.isMiss()) {
|
||||
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
|
||||
} else if (data.victimState == 1) {
|
||||
addCombatText(CombatTextEntry::DODGE, 0, 0, isPlayerAttacker);
|
||||
} else if (data.victimState == 2) {
|
||||
addCombatText(CombatTextEntry::PARRY, 0, 0, isPlayerAttacker);
|
||||
} else {
|
||||
auto type = data.isCrit() ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::MELEE_DAMAGE;
|
||||
addCombatText(type, data.totalDamage, 0, isPlayerAttacker);
|
||||
}
|
||||
|
||||
(void)isPlayerTarget; // Used for future incoming damage display
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellDamageLog(network::Packet& packet) {
|
||||
SpellDamageLogData data;
|
||||
if (!SpellDamageLogParser::parse(packet, data)) return;
|
||||
|
||||
bool isPlayerSource = (data.attackerGuid == playerGuid);
|
||||
auto type = data.isCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::SPELL_DAMAGE;
|
||||
addCombatText(type, static_cast<int32_t>(data.damage), data.spellId, isPlayerSource);
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellHealLog(network::Packet& packet) {
|
||||
SpellHealLogData data;
|
||||
if (!SpellHealLogParser::parse(packet, data)) return;
|
||||
|
||||
bool isPlayerSource = (data.casterGuid == playerGuid);
|
||||
auto type = data.isCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::HEAL;
|
||||
addCombatText(type, static_cast<int32_t>(data.heal), data.spellId, isPlayerSource);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
if (casting) return; // Already casting
|
||||
|
||||
uint64_t target = targetGuid != 0 ? targetGuid : targetGuid;
|
||||
auto packet = CastSpellPacket::build(spellId, target, ++castCount);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
||||
}
|
||||
|
||||
void GameHandler::cancelCast() {
|
||||
if (!casting) return;
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = CancelCastPacket::build(currentCastSpellId);
|
||||
socket->send(packet);
|
||||
}
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
}
|
||||
|
||||
void GameHandler::cancelAura(uint32_t spellId) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = CancelAuraPacket::build(spellId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id) {
|
||||
if (slot < 0 || slot >= ACTION_BAR_SLOTS) return;
|
||||
actionBar[slot].type = type;
|
||||
actionBar[slot].id = id;
|
||||
}
|
||||
|
||||
float GameHandler::getSpellCooldown(uint32_t spellId) const {
|
||||
auto it = spellCooldowns.find(spellId);
|
||||
return (it != spellCooldowns.end()) ? it->second : 0.0f;
|
||||
}
|
||||
|
||||
void GameHandler::handleInitialSpells(network::Packet& packet) {
|
||||
InitialSpellsData data;
|
||||
if (!InitialSpellsParser::parse(packet, data)) return;
|
||||
|
||||
knownSpells = data.spellIds;
|
||||
|
||||
// Set initial cooldowns
|
||||
for (const auto& cd : data.cooldowns) {
|
||||
if (cd.cooldownMs > 0) {
|
||||
spellCooldowns[cd.spellId] = cd.cooldownMs / 1000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-populate action bar with first 12 spells
|
||||
for (int i = 0; i < ACTION_BAR_SLOTS && i < static_cast<int>(knownSpells.size()); ++i) {
|
||||
actionBar[i].type = ActionBarSlot::SPELL;
|
||||
actionBar[i].id = knownSpells[i];
|
||||
}
|
||||
|
||||
LOG_INFO("Learned ", knownSpells.size(), " spells");
|
||||
}
|
||||
|
||||
void GameHandler::handleCastFailed(network::Packet& packet) {
|
||||
CastFailedData data;
|
||||
if (!CastFailedParser::parse(packet, data)) return;
|
||||
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
|
||||
// Add system message about failed cast
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellStart(network::Packet& packet) {
|
||||
SpellStartData data;
|
||||
if (!SpellStartParser::parse(packet, data)) return;
|
||||
|
||||
// If this is the player's own cast, start cast bar
|
||||
if (data.casterUnit == playerGuid && data.castTime > 0) {
|
||||
casting = true;
|
||||
currentCastSpellId = data.spellId;
|
||||
castTimeTotal = data.castTime / 1000.0f;
|
||||
castTimeRemaining = castTimeTotal;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellGo(network::Packet& packet) {
|
||||
SpellGoData data;
|
||||
if (!SpellGoParser::parse(packet, data)) return;
|
||||
|
||||
// Cast completed
|
||||
if (data.casterUnit == playerGuid) {
|
||||
casting = false;
|
||||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
||||
SpellCooldownData data;
|
||||
if (!SpellCooldownParser::parse(packet, data)) return;
|
||||
|
||||
for (const auto& [spellId, cooldownMs] : data.cooldowns) {
|
||||
float seconds = cooldownMs / 1000.0f;
|
||||
spellCooldowns[spellId] = seconds;
|
||||
// Update action bar cooldowns
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) {
|
||||
slot.cooldownTotal = seconds;
|
||||
slot.cooldownRemaining = seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleCooldownEvent(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
// Cooldown finished
|
||||
spellCooldowns.erase(spellId);
|
||||
for (auto& slot : actionBar) {
|
||||
if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) {
|
||||
slot.cooldownRemaining = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
||||
AuraUpdateData data;
|
||||
if (!AuraUpdateParser::parse(packet, data, isAll)) return;
|
||||
|
||||
// Determine which aura list to update
|
||||
std::vector<AuraSlot>* auraList = nullptr;
|
||||
if (data.guid == playerGuid) {
|
||||
auraList = &playerAuras;
|
||||
} else if (data.guid == targetGuid) {
|
||||
auraList = &targetAuras;
|
||||
}
|
||||
|
||||
if (auraList) {
|
||||
for (const auto& [slot, aura] : data.updates) {
|
||||
// Ensure vector is large enough
|
||||
while (auraList->size() <= slot) {
|
||||
auraList->push_back(AuraSlot{});
|
||||
}
|
||||
(*auraList)[slot] = aura;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
knownSpells.push_back(spellId);
|
||||
LOG_INFO("Learned spell: ", spellId);
|
||||
}
|
||||
|
||||
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
knownSpells.erase(
|
||||
std::remove(knownSpells.begin(), knownSpells.end(), spellId),
|
||||
knownSpells.end());
|
||||
LOG_INFO("Removed spell: ", spellId);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::inviteToGroup(const std::string& playerName) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GroupInvitePacket::build(playerName);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Inviting ", playerName, " to group");
|
||||
}
|
||||
|
||||
void GameHandler::acceptGroupInvite() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingGroupInvite = false;
|
||||
auto packet = GroupAcceptPacket::build();
|
||||
socket->send(packet);
|
||||
LOG_INFO("Accepted group invite");
|
||||
}
|
||||
|
||||
void GameHandler::declineGroupInvite() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingGroupInvite = false;
|
||||
auto packet = GroupDeclinePacket::build();
|
||||
socket->send(packet);
|
||||
LOG_INFO("Declined group invite");
|
||||
}
|
||||
|
||||
void GameHandler::leaveGroup() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GroupDisbandPacket::build();
|
||||
socket->send(packet);
|
||||
partyData = GroupListData{};
|
||||
LOG_INFO("Left group");
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupInvite(network::Packet& packet) {
|
||||
GroupInviteResponseData data;
|
||||
if (!GroupInviteResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingGroupInvite = true;
|
||||
pendingInviterName = data.inviterName;
|
||||
LOG_INFO("Group invite from: ", data.inviterName);
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupDecline(network::Packet& packet) {
|
||||
GroupDeclineData data;
|
||||
if (!GroupDeclineResponseParser::parse(packet, data)) return;
|
||||
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = data.playerName + " has declined your group invitation.";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupList(network::Packet& packet) {
|
||||
if (!GroupListParser::parse(packet, partyData)) return;
|
||||
|
||||
if (partyData.isEmpty()) {
|
||||
LOG_INFO("No longer in a group");
|
||||
} else {
|
||||
LOG_INFO("In group with ", partyData.memberCount, " members");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleGroupUninvite(network::Packet& packet) {
|
||||
(void)packet;
|
||||
partyData = GroupListData{};
|
||||
LOG_INFO("Removed from group");
|
||||
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "You have been removed from the group.";
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
void GameHandler::handlePartyCommandResult(network::Packet& packet) {
|
||||
PartyCommandResultData data;
|
||||
if (!PartyCommandResultParser::parse(packet, data)) return;
|
||||
|
||||
if (data.result != PartyResult::OK) {
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Party command failed (error " + std::to_string(static_cast<uint32_t>(data.result)) + ")";
|
||||
if (!data.name.empty()) msg.message += " for " + data.name;
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot, Gossip, Vendor
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::lootTarget(uint64_t guid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = LootPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::lootItem(uint8_t slotIndex) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = AutostoreLootItemPacket::build(slotIndex);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::closeLoot() {
|
||||
if (!lootWindowOpen) return;
|
||||
lootWindowOpen = false;
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
auto packet = LootReleasePacket::build(currentLoot.lootGuid);
|
||||
socket->send(packet);
|
||||
}
|
||||
currentLoot = LootResponseData{};
|
||||
}
|
||||
|
||||
void GameHandler::interactWithNpc(uint64_t guid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GossipHelloPacket::build(guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
||||
auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, optionId);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::closeGossip() {
|
||||
gossipWindowOpen = false;
|
||||
currentGossip = GossipMessageData{};
|
||||
}
|
||||
|
||||
void GameHandler::openVendor(uint64_t npcGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = ListInventoryPacket::build(npcGuid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = BuyItemPacket::build(vendorGuid, itemId, slot, count);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = SellItemPacket::build(vendorGuid, itemGuid, count);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
||||
lootWindowOpen = true;
|
||||
}
|
||||
|
||||
void GameHandler::handleLootReleaseResponse(network::Packet& packet) {
|
||||
(void)packet;
|
||||
lootWindowOpen = false;
|
||||
currentLoot = LootResponseData{};
|
||||
}
|
||||
|
||||
void GameHandler::handleLootRemoved(network::Packet& packet) {
|
||||
uint8_t slotIndex = packet.readUInt8();
|
||||
for (auto it = currentLoot.items.begin(); it != currentLoot.items.end(); ++it) {
|
||||
if (it->slotIndex == slotIndex) {
|
||||
currentLoot.items.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleGossipMessage(network::Packet& packet) {
|
||||
if (!GossipMessageParser::parse(packet, currentGossip)) return;
|
||||
gossipWindowOpen = true;
|
||||
vendorWindowOpen = false; // Close vendor if gossip opens
|
||||
}
|
||||
|
||||
void GameHandler::handleGossipComplete(network::Packet& packet) {
|
||||
(void)packet;
|
||||
gossipWindowOpen = false;
|
||||
currentGossip = GossipMessageData{};
|
||||
}
|
||||
|
||||
void GameHandler::handleListInventory(network::Packet& packet) {
|
||||
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
||||
vendorWindowOpen = true;
|
||||
gossipWindowOpen = false; // Close gossip if vendor opens
|
||||
}
|
||||
|
||||
uint32_t GameHandler::generateClientSeed() {
|
||||
// Generate cryptographically random seed
|
||||
std::random_device rd;
|
||||
|
|
|
|||
|
|
@ -861,5 +861,642 @@ const char* getChatTypeString(ChatType type) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Foundation — Targeting, Name Queries
|
||||
// ============================================================
|
||||
|
||||
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_SELECTION));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_SET_SELECTION: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SetActiveMoverPacket::build(uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_ACTIVE_MOVER));
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_SET_ACTIVE_MOVER: guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet NameQueryPacket::build(uint64_t playerGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_NAME_QUERY));
|
||||
packet.writeUInt64(playerGuid);
|
||||
LOG_DEBUG("Built CMSG_NAME_QUERY: guid=0x", std::hex, playerGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseData& data) {
|
||||
// 3.3.5a: packedGuid, uint8 found
|
||||
// If found==0: CString name, CString realmName, uint8 race, uint8 gender, uint8 classId
|
||||
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.found = packet.readUInt8();
|
||||
|
||||
if (data.found != 0) {
|
||||
LOG_DEBUG("Name query: player not found for GUID 0x", std::hex, data.guid, std::dec);
|
||||
return true; // Valid response, just not found
|
||||
}
|
||||
|
||||
data.name = packet.readString();
|
||||
data.realmName = packet.readString();
|
||||
data.race = packet.readUInt8();
|
||||
data.gender = packet.readUInt8();
|
||||
data.classId = packet.readUInt8();
|
||||
|
||||
LOG_INFO("Name query response: ", data.name, " (race=", (int)data.race,
|
||||
" class=", (int)data.classId, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet CreatureQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CREATURE_QUERY));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_CREATURE_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryResponseData& data) {
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means creature not found
|
||||
if (data.entry & 0x80000000) {
|
||||
data.entry &= ~0x80000000;
|
||||
LOG_DEBUG("Creature query: entry ", data.entry, " not found");
|
||||
data.name = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4 name strings (only first is usually populated)
|
||||
data.name = packet.readString();
|
||||
packet.readString(); // name2
|
||||
packet.readString(); // name3
|
||||
packet.readString(); // name4
|
||||
data.subName = packet.readString();
|
||||
data.iconName = packet.readString();
|
||||
data.typeFlags = packet.readUInt32();
|
||||
data.creatureType = packet.readUInt32();
|
||||
data.family = packet.readUInt32();
|
||||
data.rank = packet.readUInt32();
|
||||
|
||||
// Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.)
|
||||
// We've got what we need for display purposes
|
||||
|
||||
LOG_INFO("Creature query response: ", data.name, " (type=", data.creatureType,
|
||||
" rank=", data.rank, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat Core
|
||||
// ============================================================
|
||||
|
||||
network::Packet AttackSwingPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSWING));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_ATTACKSWING: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AttackStopPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSTOP));
|
||||
LOG_DEBUG("Built CMSG_ATTACKSTOP");
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) {
|
||||
if (packet.getSize() < 16) return false;
|
||||
data.attackerGuid = packet.readUInt64();
|
||||
data.victimGuid = packet.readUInt64();
|
||||
LOG_INFO("Attack started: 0x", std::hex, data.attackerGuid,
|
||||
" -> 0x", data.victimGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) {
|
||||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.victimGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
data.unknown = packet.readUInt32();
|
||||
}
|
||||
LOG_INFO("Attack stopped: 0x", std::hex, data.attackerGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
|
||||
data.hitInfo = packet.readUInt32();
|
||||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
||||
data.subDamageCount = packet.readUInt8();
|
||||
|
||||
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
|
||||
SubDamage sub;
|
||||
sub.schoolMask = packet.readUInt32();
|
||||
sub.damage = packet.readFloat();
|
||||
sub.intDamage = packet.readUInt32();
|
||||
sub.absorbed = packet.readUInt32();
|
||||
sub.resisted = packet.readUInt32();
|
||||
data.subDamages.push_back(sub);
|
||||
}
|
||||
|
||||
data.victimState = packet.readUInt32();
|
||||
data.overkill = static_cast<int32_t>(packet.readUInt32());
|
||||
|
||||
// Read blocked amount
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
data.blocked = packet.readUInt32();
|
||||
}
|
||||
|
||||
LOG_INFO("Melee hit: ", data.totalDamage, " damage",
|
||||
data.isCrit() ? " (CRIT)" : "",
|
||||
data.isMiss() ? " (MISS)" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) {
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.spellId = packet.readUInt32();
|
||||
data.damage = packet.readUInt32();
|
||||
data.overkill = packet.readUInt32();
|
||||
data.schoolMask = packet.readUInt8();
|
||||
data.absorbed = packet.readUInt32();
|
||||
data.resisted = packet.readUInt32();
|
||||
|
||||
// Skip remaining fields
|
||||
uint8_t periodicLog = packet.readUInt8();
|
||||
(void)periodicLog;
|
||||
packet.readUInt8(); // unused
|
||||
packet.readUInt32(); // blocked
|
||||
uint32_t flags = packet.readUInt32();
|
||||
(void)flags;
|
||||
// Check crit flag
|
||||
data.isCrit = (flags & 0x02) != 0;
|
||||
|
||||
LOG_INFO("Spell damage: spellId=", data.spellId, " dmg=", data.damage,
|
||||
data.isCrit ? " CRIT" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) {
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.spellId = packet.readUInt32();
|
||||
data.heal = packet.readUInt32();
|
||||
data.overheal = packet.readUInt32();
|
||||
data.absorbed = packet.readUInt32();
|
||||
uint8_t critFlag = packet.readUInt8();
|
||||
data.isCrit = (critFlag != 0);
|
||||
|
||||
LOG_INFO("Spell heal: spellId=", data.spellId, " heal=", data.heal,
|
||||
data.isCrit ? " CRIT" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data) {
|
||||
data.talentSpec = packet.readUInt8();
|
||||
uint16_t spellCount = packet.readUInt16();
|
||||
|
||||
data.spellIds.reserve(spellCount);
|
||||
for (uint16_t i = 0; i < spellCount; ++i) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
packet.readUInt16(); // unknown (always 0)
|
||||
if (spellId != 0) {
|
||||
data.spellIds.push_back(spellId);
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t cooldownCount = packet.readUInt16();
|
||||
data.cooldowns.reserve(cooldownCount);
|
||||
for (uint16_t i = 0; i < cooldownCount; ++i) {
|
||||
SpellCooldownEntry entry;
|
||||
entry.spellId = packet.readUInt32();
|
||||
entry.itemId = packet.readUInt16();
|
||||
entry.categoryId = packet.readUInt16();
|
||||
entry.cooldownMs = packet.readUInt32();
|
||||
entry.categoryCooldownMs = packet.readUInt32();
|
||||
data.cooldowns.push_back(entry);
|
||||
}
|
||||
|
||||
LOG_INFO("Initial spells: ", data.spellIds.size(), " spells, ",
|
||||
data.cooldowns.size(), " cooldowns");
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CAST_SPELL));
|
||||
packet.writeUInt8(castCount);
|
||||
packet.writeUInt32(spellId);
|
||||
packet.writeUInt8(0x00); // castFlags = 0 for normal cast
|
||||
|
||||
// SpellCastTargets
|
||||
if (targetGuid != 0) {
|
||||
packet.writeUInt32(0x02); // TARGET_FLAG_UNIT
|
||||
|
||||
// Write packed GUID
|
||||
uint8_t mask = 0;
|
||||
uint8_t bytes[8];
|
||||
int byteCount = 0;
|
||||
uint64_t g = targetGuid;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
uint8_t b = g & 0xFF;
|
||||
if (b != 0) {
|
||||
mask |= (1 << i);
|
||||
bytes[byteCount++] = b;
|
||||
}
|
||||
g >>= 8;
|
||||
}
|
||||
packet.writeUInt8(mask);
|
||||
for (int i = 0; i < byteCount; ++i) {
|
||||
packet.writeUInt8(bytes[i]);
|
||||
}
|
||||
} else {
|
||||
packet.writeUInt32(0x00); // TARGET_FLAG_SELF
|
||||
}
|
||||
|
||||
LOG_DEBUG("Built CMSG_CAST_SPELL: spell=", spellId, " target=0x",
|
||||
std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet CancelCastPacket::build(uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_CAST));
|
||||
packet.writeUInt32(0); // sequence
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet CancelAuraPacket::build(uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_AURA));
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) {
|
||||
data.castCount = packet.readUInt8();
|
||||
data.spellId = packet.readUInt32();
|
||||
data.result = packet.readUInt8();
|
||||
LOG_INFO("Cast failed: spell=", data.spellId, " result=", (int)data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
|
||||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.castCount = packet.readUInt8();
|
||||
data.spellId = packet.readUInt32();
|
||||
data.castFlags = packet.readUInt32();
|
||||
data.castTime = packet.readUInt32();
|
||||
|
||||
// Read target flags and target (simplified)
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
uint32_t targetFlags = packet.readUInt32();
|
||||
if (targetFlags & 0x02) { // TARGET_FLAG_UNIT
|
||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
||||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.castCount = packet.readUInt8();
|
||||
data.spellId = packet.readUInt32();
|
||||
data.castFlags = packet.readUInt32();
|
||||
// Timestamp in 3.3.5a
|
||||
packet.readUInt32();
|
||||
|
||||
data.hitCount = packet.readUInt8();
|
||||
data.hitTargets.reserve(data.hitCount);
|
||||
for (uint8_t i = 0; i < data.hitCount; ++i) {
|
||||
data.hitTargets.push_back(packet.readUInt64());
|
||||
}
|
||||
|
||||
data.missCount = packet.readUInt8();
|
||||
// Skip miss details for now
|
||||
|
||||
LOG_INFO("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||
" misses=", (int)data.missCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool isAll) {
|
||||
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
|
||||
while (packet.getReadPos() < packet.getSize()) {
|
||||
uint8_t slot = packet.readUInt8();
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
|
||||
AuraSlot aura;
|
||||
if (spellId != 0) {
|
||||
aura.spellId = spellId;
|
||||
aura.flags = packet.readUInt8();
|
||||
aura.level = packet.readUInt8();
|
||||
aura.charges = packet.readUInt8();
|
||||
|
||||
if (!(aura.flags & 0x08)) { // NOT_CASTER flag
|
||||
aura.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
if (aura.flags & 0x20) { // DURATION
|
||||
aura.maxDurationMs = static_cast<int32_t>(packet.readUInt32());
|
||||
aura.durationMs = static_cast<int32_t>(packet.readUInt32());
|
||||
}
|
||||
|
||||
if (aura.flags & 0x40) { // EFFECT_AMOUNTS - skip
|
||||
// 3 effect amounts
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
packet.readUInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.updates.push_back({slot, aura});
|
||||
|
||||
// For single update, only one entry
|
||||
if (!isAll) break;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Aura update for 0x", std::hex, data.guid, std::dec,
|
||||
": ", data.updates.size(), " slots");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data) {
|
||||
data.guid = packet.readUInt64();
|
||||
data.flags = packet.readUInt8();
|
||||
|
||||
while (packet.getReadPos() + 8 <= packet.getSize()) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
uint32_t cooldownMs = packet.readUInt32();
|
||||
data.cooldowns.push_back({spellId, cooldownMs});
|
||||
}
|
||||
|
||||
LOG_DEBUG("Spell cooldowns: ", data.cooldowns.size(), " entries");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party System
|
||||
// ============================================================
|
||||
|
||||
network::Packet GroupInvitePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_INVITE));
|
||||
packet.writeString(playerName);
|
||||
packet.writeUInt32(0); // unused
|
||||
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteResponseData& data) {
|
||||
data.canAccept = packet.readUInt8();
|
||||
data.inviterName = packet.readString();
|
||||
LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", (int)data.canAccept, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
network::Packet GroupAcceptPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_ACCEPT));
|
||||
packet.writeUInt32(0); // unused in 3.3.5a
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GroupDeclinePacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DECLINE));
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GroupDisbandPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DISBAND));
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GroupListParser::parse(network::Packet& packet, GroupListData& data) {
|
||||
data.groupType = packet.readUInt8();
|
||||
data.subGroup = packet.readUInt8();
|
||||
data.flags = packet.readUInt8();
|
||||
data.roles = packet.readUInt8();
|
||||
|
||||
// Skip LFG data if present
|
||||
if (data.groupType & 0x04) {
|
||||
packet.readUInt8(); // lfg state
|
||||
packet.readUInt32(); // lfg entry
|
||||
packet.readUInt8(); // lfg flags (3.3.5a may not have this)
|
||||
}
|
||||
|
||||
packet.readUInt64(); // group GUID
|
||||
packet.readUInt32(); // counter
|
||||
|
||||
data.memberCount = packet.readUInt32();
|
||||
data.members.reserve(data.memberCount);
|
||||
|
||||
for (uint32_t i = 0; i < data.memberCount; ++i) {
|
||||
GroupMember member;
|
||||
member.name = packet.readString();
|
||||
member.guid = packet.readUInt64();
|
||||
member.isOnline = packet.readUInt8();
|
||||
member.subGroup = packet.readUInt8();
|
||||
member.flags = packet.readUInt8();
|
||||
member.roles = packet.readUInt8();
|
||||
data.members.push_back(member);
|
||||
}
|
||||
|
||||
data.leaderGuid = packet.readUInt64();
|
||||
|
||||
if (data.memberCount > 0 && packet.getReadPos() < packet.getSize()) {
|
||||
data.lootMethod = packet.readUInt8();
|
||||
data.looterGuid = packet.readUInt64();
|
||||
data.lootThreshold = packet.readUInt8();
|
||||
data.difficultyId = packet.readUInt8();
|
||||
data.raidDifficultyId = packet.readUInt8();
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
packet.readUInt8(); // unknown byte
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Group list: ", data.memberCount, " members, leader=0x",
|
||||
std::hex, data.leaderGuid, std::dec);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PartyCommandResultParser::parse(network::Packet& packet, PartyCommandResultData& data) {
|
||||
data.command = static_cast<PartyCommand>(packet.readUInt32());
|
||||
data.name = packet.readString();
|
||||
data.result = static_cast<PartyResult>(packet.readUInt32());
|
||||
LOG_INFO("Party command result: ", (int)data.result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData& data) {
|
||||
data.playerName = packet.readString();
|
||||
LOG_INFO("Group decline from: ", data.playerName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot System
|
||||
// ============================================================
|
||||
|
||||
network::Packet LootPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
|
||||
packet.writeUInt8(slotIndex);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_RELEASE));
|
||||
packet.writeUInt64(lootGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) {
|
||||
data.lootGuid = packet.readUInt64();
|
||||
data.lootType = packet.readUInt8();
|
||||
data.gold = packet.readUInt32();
|
||||
uint8_t itemCount = packet.readUInt8();
|
||||
|
||||
data.items.reserve(itemCount);
|
||||
for (uint8_t i = 0; i < itemCount; ++i) {
|
||||
LootItem item;
|
||||
item.slotIndex = packet.readUInt8();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
item.randomSuffix = packet.readUInt32();
|
||||
item.randomPropertyId = packet.readUInt32();
|
||||
item.lootSlotType = packet.readUInt8();
|
||||
data.items.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO("Loot response: ", (int)itemCount, " items, ", data.gold, " copper");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: NPC Gossip
|
||||
// ============================================================
|
||||
|
||||
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_HELLO));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optionId, const std::string& code) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(optionId);
|
||||
if (!code.empty()) {
|
||||
packet.writeString(code);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
|
||||
data.npcGuid = packet.readUInt64();
|
||||
data.menuId = packet.readUInt32();
|
||||
data.titleTextId = packet.readUInt32();
|
||||
uint32_t optionCount = packet.readUInt32();
|
||||
|
||||
data.options.reserve(optionCount);
|
||||
for (uint32_t i = 0; i < optionCount; ++i) {
|
||||
GossipOption opt;
|
||||
opt.id = packet.readUInt32();
|
||||
opt.icon = packet.readUInt8();
|
||||
opt.isCoded = (packet.readUInt8() != 0);
|
||||
opt.boxMoney = packet.readUInt32();
|
||||
opt.text = packet.readString();
|
||||
opt.boxText = packet.readString();
|
||||
data.options.push_back(opt);
|
||||
}
|
||||
|
||||
uint32_t questCount = packet.readUInt32();
|
||||
data.quests.reserve(questCount);
|
||||
for (uint32_t i = 0; i < questCount; ++i) {
|
||||
GossipQuestItem quest;
|
||||
quest.questId = packet.readUInt32();
|
||||
quest.questIcon = packet.readUInt32();
|
||||
quest.questLevel = static_cast<int32_t>(packet.readUInt32());
|
||||
quest.questFlags = packet.readUInt32();
|
||||
quest.isRepeatable = packet.readUInt8();
|
||||
quest.title = packet.readString();
|
||||
data.quests.push_back(quest);
|
||||
}
|
||||
|
||||
LOG_INFO("Gossip: ", optionCount, " options, ", questCount, " quests");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Vendor
|
||||
// ============================================================
|
||||
|
||||
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LIST_INVENTORY));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_BUY_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt32(itemId);
|
||||
packet.writeUInt32(slot);
|
||||
packet.writeUInt8(count);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SELL_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt8(count);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) {
|
||||
data.vendorGuid = packet.readUInt64();
|
||||
uint8_t itemCount = packet.readUInt8();
|
||||
|
||||
if (itemCount == 0) {
|
||||
LOG_INFO("Vendor has nothing for sale");
|
||||
return true;
|
||||
}
|
||||
|
||||
data.items.reserve(itemCount);
|
||||
for (uint8_t i = 0; i < itemCount; ++i) {
|
||||
VendorItem item;
|
||||
item.slot = packet.readUInt32();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
item.maxCount = static_cast<int32_t>(packet.readUInt32());
|
||||
item.buyPrice = packet.readUInt32();
|
||||
item.durability = packet.readUInt32();
|
||||
item.stackCount = packet.readUInt32();
|
||||
item.extendedCost = packet.readUInt32();
|
||||
data.items.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO("Vendor inventory: ", (int)itemCount, " items");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue