diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 14da2aa2..51a4b4d1 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -151,6 +151,17 @@ public: uint32_t getDisplayId() const { return displayId; } void setDisplayId(uint32_t id) { displayId = id; } + // Unit flags (UNIT_FIELD_FLAGS, index 59) + uint32_t getUnitFlags() const { return unitFlags; } + void setUnitFlags(uint32_t f) { unitFlags = f; } + + // NPC flags (UNIT_NPC_FLAGS, index 82) + uint32_t getNpcFlags() const { return npcFlags; } + void setNpcFlags(uint32_t f) { npcFlags = f; } + + // Returns true if NPC has interaction flags (gossip/vendor/quest/trainer) + bool isInteractable() const { return npcFlags != 0; } + protected: std::string name; uint32_t health = 0; @@ -161,6 +172,8 @@ protected: uint32_t level = 1; uint32_t entry = 0; uint32_t displayId = 0; + uint32_t unitFlags = 0; + uint32_t npcFlags = 0; }; /** diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 770a42a9..f4783a7a 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -410,6 +410,9 @@ private: // ---- Phase 1 handlers ---- void handleNameQueryResponse(network::Packet& packet); void handleCreatureQueryResponse(network::Packet& packet); + void handleItemQueryResponse(network::Packet& packet); + void queryItemInfo(uint32_t entry, uint64_t guid); + void rebuildOnlineInventory(); // ---- Phase 2 handlers ---- void handleAttackStart(network::Packet& packet); @@ -535,6 +538,17 @@ private: std::unordered_map creatureInfoCache; std::unordered_set pendingCreatureQueries; + // ---- Online item tracking ---- + struct OnlineItemInfo { + uint32_t entry = 0; + uint32_t stackCount = 1; + }; + std::unordered_map onlineItems_; + std::unordered_map itemInfoCache_; + std::unordered_set pendingItemQueries_; + std::array equipSlotGuids_{}; + std::array backpackSlotGuids_{}; + // ---- Phase 2: Combat ---- bool autoAttacking = false; uint64_t autoAttackTarget = 0; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index ada6889b..414fa20f 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -139,6 +139,8 @@ enum class Opcode : uint16_t { SMSG_BUY_FAILED = 0x1A5, // ---- Phase 5: Item/Equip ---- + CMSG_ITEM_QUERY_SINGLE = 0x056, + SMSG_ITEM_QUERY_SINGLE_RESPONSE = 0x058, CMSG_AUTOEQUIP_ITEM = 0x10A, SMSG_INVENTORY_CHANGE_FAILURE = 0x112, }; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index fb00bb7d..645d02dd 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -715,6 +715,41 @@ public: static bool parse(network::Packet& packet, CreatureQueryResponseData& data); }; +// ============================================================ +// Item Query +// ============================================================ + +/** CMSG_ITEM_QUERY_SINGLE packet builder */ +class ItemQueryPacket { +public: + static network::Packet build(uint32_t entry, uint64_t guid); +}; + +/** SMSG_ITEM_QUERY_SINGLE_RESPONSE data */ +struct ItemQueryResponseData { + uint32_t entry = 0; + std::string name; + uint32_t displayInfoId = 0; + uint32_t quality = 0; + uint32_t inventoryType = 0; + int32_t maxStack = 1; + uint32_t containerSlots = 0; + int32_t armor = 0; + int32_t stamina = 0; + int32_t strength = 0; + int32_t agility = 0; + int32_t intellect = 0; + int32_t spirit = 0; + std::string subclassName; + bool valid = false; +}; + +/** SMSG_ITEM_QUERY_SINGLE_RESPONSE parser */ +class ItemQueryResponseParser { +public: + static bool parse(network::Packet& packet, ItemQueryResponseData& data); +}; + // ============================================================ // Phase 2: Combat Core // ============================================================ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 1fee33b1..0ed8396e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -964,6 +964,10 @@ void GameHandler::handlePacket(network::Packet& packet) { handleCreatureQueryResponse(packet); break; + case Opcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE: + handleItemQueryResponse(packet); + break; + // ---- XP ---- case Opcode::SMSG_LOG_XPGAIN: handleXpGain(packet); @@ -2382,8 +2386,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { case 25: unit->setPower(val); break; case 32: unit->setMaxHealth(val); break; case 33: unit->setMaxPower(val); break; + case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS case 54: unit->setLevel(val); break; case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID + case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS default: break; } } @@ -2395,16 +2401,50 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } } - // Extract XP fields for player entity + // Track online item objects + if (block.objectType == ObjectType::ITEM) { + auto entryIt = block.fields.find(3); // OBJECT_FIELD_ENTRY + auto stackIt = block.fields.find(14); // ITEM_FIELD_STACK_COUNT + if (entryIt != block.fields.end() && entryIt->second != 0) { + OnlineItemInfo info; + info.entry = entryIt->second; + info.stackCount = (stackIt != block.fields.end()) ? stackIt->second : 1; + onlineItems_[block.guid] = info; + queryItemInfo(info.entry, block.guid); + } + } + + // Extract XP / inventory slot fields for player entity if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { + bool slotsChanged = false; for (const auto& [key, val] : block.fields) { - switch (key) { - case 634: playerXp_ = val; break; // PLAYER_XP - case 635: playerNextLevelXp_ = val; break; // PLAYER_NEXT_LEVEL_XP - case 54: serverPlayerLevel_ = val; break; // UNIT_FIELD_LEVEL - default: break; + if (key == 634) { playerXp_ = val; } // PLAYER_XP + else if (key == 635) { playerNextLevelXp_ = val; } // PLAYER_NEXT_LEVEL_XP + else if (key == 54) { serverPlayerLevel_ = val; } // UNIT_FIELD_LEVEL + else if (key == 632) { playerMoneyCopper_ = val; } // PLAYER_FIELD_COINAGE + else if (key >= 322 && key <= 367) { + // PLAYER_FIELD_INV_SLOT_HEAD: equipment slots (23 slots × 2 fields) + int slotIndex = (key - 322) / 2; + bool isLow = ((key - 322) % 2 == 0); + if (slotIndex < 23) { + uint64_t& guid = equipSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } + } else if (key >= 368 && key <= 399) { + // PLAYER_FIELD_PACK_SLOT_1: backpack slots (16 slots × 2 fields) + int slotIndex = (key - 368) / 2; + bool isLow = ((key - 368) % 2 == 0); + if (slotIndex < 16) { + uint64_t& guid = backpackSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } } } + if (slotsChanged) rebuildOnlineInventory(); } break; } @@ -2422,25 +2462,62 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { auto unit = std::static_pointer_cast(entity); for (const auto& [key, val] : block.fields) { switch (key) { - case 24: unit->setHealth(val); break; + case 24: + unit->setHealth(val); + if (val == 0 && block.guid == autoAttackTarget) { + stopAutoAttack(); + } + break; case 25: unit->setPower(val); break; case 32: unit->setMaxHealth(val); break; case 33: unit->setMaxPower(val); break; + case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS case 54: unit->setLevel(val); break; + case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS default: break; } } } - // Update XP fields for player entity + // Update XP / inventory slot fields for player entity if (block.guid == playerGuid) { + bool slotsChanged = false; for (const auto& [key, val] : block.fields) { - switch (key) { - case 634: playerXp_ = val; break; // PLAYER_XP - case 635: playerNextLevelXp_ = val; break; // PLAYER_NEXT_LEVEL_XP - case 54: serverPlayerLevel_ = val; break; // UNIT_FIELD_LEVEL - default: break; + if (key == 634) { playerXp_ = val; } + else if (key == 635) { playerNextLevelXp_ = val; } + else if (key == 54) { serverPlayerLevel_ = val; } + else if (key == 632) { playerMoneyCopper_ = val; } + else if (key >= 322 && key <= 367) { + int slotIndex = (key - 322) / 2; + bool isLow = ((key - 322) % 2 == 0); + if (slotIndex < 23) { + uint64_t& guid = equipSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } + } else if (key >= 368 && key <= 399) { + int slotIndex = (key - 368) / 2; + bool isLow = ((key - 368) % 2 == 0); + if (slotIndex < 16) { + uint64_t& guid = backpackSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } } } + if (slotsChanged) rebuildOnlineInventory(); + } + + // Update item stack count for online items + if (entity->getType() == ObjectType::ITEM) { + for (const auto& [key, val] : block.fields) { + if (key == 14) { // ITEM_FIELD_STACK_COUNT + auto it = onlineItems_.find(block.guid); + if (it != onlineItems_.end()) it->second.stackCount = val; + } + } + rebuildOnlineInventory(); } LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec); @@ -2528,6 +2605,19 @@ void GameHandler::handleDestroyObject(network::Packet& packet) { LOG_WARNING("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec); } + // Clean up auto-attack and target if destroyed entity was our target + if (data.guid == autoAttackTarget) { + stopAutoAttack(); + } + if (data.guid == targetGuid) { + targetGuid = 0; + } + + // Remove online item tracking + if (onlineItems_.erase(data.guid)) { + rebuildOnlineInventory(); + } + tabCycleStale = true; LOG_INFO("Entity count: ", entityManager.getEntityCount()); } @@ -2748,6 +2838,111 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) { } } +// ============================================================ +// Item Query +// ============================================================ + +void GameHandler::queryItemInfo(uint32_t entry, uint64_t guid) { + if (itemInfoCache_.count(entry) || pendingItemQueries_.count(entry)) return; + if (state != WorldState::IN_WORLD || !socket) return; + + pendingItemQueries_.insert(entry); + auto packet = ItemQueryPacket::build(entry, guid); + socket->send(packet); +} + +void GameHandler::handleItemQueryResponse(network::Packet& packet) { + ItemQueryResponseData data; + if (!ItemQueryResponseParser::parse(packet, data)) return; + + pendingItemQueries_.erase(data.entry); + + if (data.valid) { + itemInfoCache_[data.entry] = data; + rebuildOnlineInventory(); + } +} + +void GameHandler::rebuildOnlineInventory() { + if (singlePlayerMode_) return; + + inventory = Inventory(); + + // Equipment slots + for (int i = 0; i < 23; i++) { + uint64_t guid = equipSlotGuids_[i]; + if (guid == 0) continue; + + auto itemIt = onlineItems_.find(guid); + if (itemIt == onlineItems_.end()) continue; + + ItemDef def; + def.itemId = itemIt->second.entry; + def.stackCount = itemIt->second.stackCount; + def.maxStack = 1; + + auto infoIt = itemInfoCache_.find(itemIt->second.entry); + if (infoIt != itemInfoCache_.end()) { + def.name = infoIt->second.name; + def.quality = static_cast(infoIt->second.quality); + def.inventoryType = infoIt->second.inventoryType; + def.maxStack = std::max(1, infoIt->second.maxStack); + def.displayInfoId = infoIt->second.displayInfoId; + def.subclassName = infoIt->second.subclassName; + def.armor = infoIt->second.armor; + def.stamina = infoIt->second.stamina; + def.strength = infoIt->second.strength; + def.agility = infoIt->second.agility; + def.intellect = infoIt->second.intellect; + def.spirit = infoIt->second.spirit; + } else { + def.name = "Item " + std::to_string(def.itemId); + } + + inventory.setEquipSlot(static_cast(i), def); + } + + // Backpack slots + for (int i = 0; i < 16; i++) { + uint64_t guid = backpackSlotGuids_[i]; + if (guid == 0) continue; + + auto itemIt = onlineItems_.find(guid); + if (itemIt == onlineItems_.end()) continue; + + ItemDef def; + def.itemId = itemIt->second.entry; + def.stackCount = itemIt->second.stackCount; + def.maxStack = 1; + + auto infoIt = itemInfoCache_.find(itemIt->second.entry); + if (infoIt != itemInfoCache_.end()) { + def.name = infoIt->second.name; + def.quality = static_cast(infoIt->second.quality); + def.inventoryType = infoIt->second.inventoryType; + def.maxStack = std::max(1, infoIt->second.maxStack); + def.displayInfoId = infoIt->second.displayInfoId; + def.subclassName = infoIt->second.subclassName; + def.armor = infoIt->second.armor; + def.stamina = infoIt->second.stamina; + def.strength = infoIt->second.strength; + def.agility = infoIt->second.agility; + def.intellect = infoIt->second.intellect; + def.spirit = infoIt->second.spirit; + } else { + def.name = "Item " + std::to_string(def.itemId); + } + + inventory.setBackpackSlot(i, def); + } + + LOG_DEBUG("Rebuilt online inventory: equip=", [&](){ + int c = 0; for (auto g : equipSlotGuids_) if (g) c++; return c; + }(), " backpack=", [&](){ + int c = 0; for (auto g : backpackSlotGuids_) if (g) c++; return c; + }()); +} + // ============================================================ // Phase 2: Combat // ============================================================ diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index b5c4297a..bffc5fff 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1207,6 +1207,121 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe return true; } +// ---- Item Query ---- + +network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) { + network::Packet packet(static_cast(Opcode::CMSG_ITEM_QUERY_SINGLE)); + packet.writeUInt32(entry); + packet.writeUInt64(guid); + LOG_DEBUG("Built CMSG_ITEM_QUERY_SINGLE: entry=", entry, " guid=0x", std::hex, guid, std::dec); + return packet; +} + +static const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass) { + if (itemClass == 2) { // Weapon + switch (subClass) { + case 0: return "Axe"; case 1: return "Axe"; + case 2: return "Bow"; case 3: return "Gun"; + case 4: return "Mace"; case 5: return "Mace"; + case 6: return "Polearm"; case 7: return "Sword"; + case 8: return "Sword"; case 9: return "Obsolete"; + case 10: return "Staff"; case 13: return "Fist Weapon"; + case 15: return "Dagger"; case 16: return "Thrown"; + case 18: return "Crossbow"; case 19: return "Wand"; + case 20: return "Fishing Pole"; + default: return "Weapon"; + } + } + if (itemClass == 4) { // Armor + switch (subClass) { + case 0: return "Miscellaneous"; case 1: return "Cloth"; + case 2: return "Leather"; case 3: return "Mail"; + case 4: return "Plate"; case 6: return "Shield"; + default: return "Armor"; + } + } + return ""; +} + +bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseData& data) { + data.entry = packet.readUInt32(); + + // High bit set means item not found + if (data.entry & 0x80000000) { + data.entry &= ~0x80000000; + LOG_DEBUG("Item query: entry ", data.entry, " not found"); + return true; + } + + uint32_t itemClass = packet.readUInt32(); + uint32_t subClass = packet.readUInt32(); + packet.readUInt32(); // SoundOverrideSubclass + + data.subclassName = getItemSubclassName(itemClass, subClass); + + // 4 name strings + data.name = packet.readString(); + packet.readString(); // name2 + packet.readString(); // name3 + packet.readString(); // name4 + + data.displayInfoId = packet.readUInt32(); + data.quality = packet.readUInt32(); + + packet.readUInt32(); // Flags + packet.readUInt32(); // Flags2 + packet.readUInt32(); // BuyPrice + packet.readUInt32(); // SellPrice + + data.inventoryType = packet.readUInt32(); + + packet.readUInt32(); // AllowableClass + packet.readUInt32(); // AllowableRace + packet.readUInt32(); // ItemLevel + packet.readUInt32(); // RequiredLevel + packet.readUInt32(); // RequiredSkill + packet.readUInt32(); // RequiredSkillRank + packet.readUInt32(); // RequiredSpell + packet.readUInt32(); // RequiredHonorRank + packet.readUInt32(); // RequiredCityRank + packet.readUInt32(); // RequiredReputationFaction + packet.readUInt32(); // RequiredReputationRank + packet.readUInt32(); // MaxCount + data.maxStack = static_cast(packet.readUInt32()); // Stackable + data.containerSlots = packet.readUInt32(); + + uint32_t statsCount = packet.readUInt32(); + for (uint32_t i = 0; i < statsCount && i < 10; i++) { + uint32_t statType = packet.readUInt32(); + int32_t statValue = static_cast(packet.readUInt32()); + switch (statType) { + case 3: data.agility = statValue; break; + case 4: data.strength = statValue; break; + case 5: data.intellect = statValue; break; + case 6: data.spirit = statValue; break; + case 7: data.stamina = statValue; break; + default: break; + } + } + + packet.readUInt32(); // ScalingStatDistribution + packet.readUInt32(); // ScalingStatValue + + // 5 damage types + for (int i = 0; i < 5; i++) { + packet.readFloat(); // DamageMin + packet.readFloat(); // DamageMax + packet.readUInt32(); // DamageType + } + + data.armor = static_cast(packet.readUInt32()); + + data.valid = !data.name.empty(); + LOG_INFO("Item query response: ", data.name, " (quality=", data.quality, + " invType=", data.inventoryType, " stack=", data.maxStack, ")"); + return true; +} + // ============================================================ // Phase 2: Combat Core // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 4ef8a967..64a9f0eb 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -462,8 +462,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { gameHandler.startAutoAttack(target->getGuid()); } } else { - // Try NPC interaction first (gossip), fall back to attack - gameHandler.interactWithNpc(target->getGuid()); + // Online mode: interact with friendly NPCs, attack hostiles + if (unit->isInteractable()) { + gameHandler.interactWithNpc(target->getGuid()); + } else { + if (gameHandler.isAutoAttacking()) { + gameHandler.stopAutoAttack(); + } else { + gameHandler.startAutoAttack(target->getGuid()); + } + } } } else if (target->getType() == game::ObjectType::PLAYER) { // Right-click another player could start attack in PvP context