From 89ccb0720af2738105cbf433e3b7192ba6aa2f51 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Feb 2026 22:14:34 -0800 Subject: [PATCH] Fix vanilla spell casting and bag contents Vanilla CMSG_CAST_SPELL target mask is uint16 (not uint32 like WotLK), the extra 2 bytes were corrupting packets. Also implement full bag content tracking: extract container slot GUIDs from CONTAINER update objects, set proper bag sizes, and populate bag items in inventory rebuild. --- Data/expansions/classic/update_fields.json | 4 +- Data/expansions/tbc/update_fields.json | 4 +- Data/expansions/turtle/update_fields.json | 4 +- Data/expansions/wotlk/update_fields.json | 4 +- include/game/game_handler.hpp | 7 ++ include/game/inventory.hpp | 1 + include/game/packet_parsers.hpp | 6 ++ include/game/update_field_table.hpp | 4 + src/game/game_handler.cpp | 119 ++++++++++++++++++++- src/game/inventory.cpp | 5 + src/game/packet_parsers_classic.cpp | 38 +++++++ src/game/update_field_table.cpp | 4 + 12 files changed, 191 insertions(+), 9 deletions(-) diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json index 04ee8abd..b9d33bd6 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -28,5 +28,7 @@ "PLAYER_EXPLORED_ZONES_START": 1111, "PLAYER_END": 1282, "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14 + "ITEM_FIELD_STACK_COUNT": 14, + "CONTAINER_FIELD_NUM_SLOTS": 48, + "CONTAINER_FIELD_SLOT_1": 50 } diff --git a/Data/expansions/tbc/update_fields.json b/Data/expansions/tbc/update_fields.json index 36d17a2c..c4335ad3 100644 --- a/Data/expansions/tbc/update_fields.json +++ b/Data/expansions/tbc/update_fields.json @@ -28,5 +28,7 @@ "PLAYER_SKILL_INFO_START": 928, "PLAYER_EXPLORED_ZONES_START": 1312, "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14 + "ITEM_FIELD_STACK_COUNT": 14, + "CONTAINER_FIELD_NUM_SLOTS": 64, + "CONTAINER_FIELD_SLOT_1": 66 } diff --git a/Data/expansions/turtle/update_fields.json b/Data/expansions/turtle/update_fields.json index 04ee8abd..b9d33bd6 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -28,5 +28,7 @@ "PLAYER_EXPLORED_ZONES_START": 1111, "PLAYER_END": 1282, "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14 + "ITEM_FIELD_STACK_COUNT": 14, + "CONTAINER_FIELD_NUM_SLOTS": 48, + "CONTAINER_FIELD_SLOT_1": 50 } diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index 2b85031f..93855773 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -28,5 +28,7 @@ "PLAYER_SKILL_INFO_START": 636, "PLAYER_EXPLORED_ZONES_START": 1041, "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14 + "ITEM_FIELD_STACK_COUNT": 14, + "CONTAINER_FIELD_NUM_SLOTS": 64, + "CONTAINER_FIELD_SLOT_1": 66 } diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 1b609853..97ea1b61 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -859,6 +859,7 @@ private: void emitAllOtherPlayerEquipment(); void detectInventorySlotBases(const std::map& fields); bool applyInventoryFields(const std::map& fields); + void extractContainerFields(uint64_t containerGuid, const std::map& fields); uint64_t resolveOnlineItemGuid(uint32_t itemId) const; // ---- Phase 2 handlers ---- @@ -1094,6 +1095,12 @@ private: std::unordered_set pendingItemQueries_; std::array equipSlotGuids_{}; std::array backpackSlotGuids_{}; + // Container (bag) contents: containerGuid -> array of item GUIDs per slot + struct ContainerInfo { + uint32_t numSlots = 0; + std::array slotGuids{}; // max 36 slots + }; + std::unordered_map containerContents_; int invSlotBase_ = -1; int packSlotBase_ = -1; std::map lastPlayerFields_; diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index e4edf8a3..45bf5952 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -72,6 +72,7 @@ public: // Extra bags int getBagSize(int bagIndex) const; + void setBagSize(int bagIndex, int size); const ItemSlot& getBagSlot(int bagIndex, int slotIndex) const; bool setBagSlot(int bagIndex, int slotIndex, const ItemDef& item); diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 5652f6f8..41b4b9be 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -45,6 +45,11 @@ public: return MovementPacket::build(opcode, info, playerGuid); } + /** Build CMSG_CAST_SPELL (WotLK default: castCount + spellId + castFlags + targets) */ + virtual network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) { + return CastSpellPacket::build(spellId, targetGuid, castCount); + } + // --- Character Enumeration --- /** Parse SMSG_CHAR_ENUM */ @@ -201,6 +206,7 @@ public: network::Packet buildMovementPacket(LogicalOpcode opcode, const MovementInfo& info, uint64_t playerGuid = 0) override; + network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override; bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override; bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override; diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 3b5be7f2..63f2cbf1 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -52,6 +52,10 @@ enum class UF : uint16_t { // Item fields ITEM_FIELD_STACK_COUNT, + // Container fields + CONTAINER_FIELD_NUM_SLOTS, + CONTAINER_FIELD_SLOT_1, + COUNT }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 53276882..a3f6d17f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3161,8 +3161,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { go->getX(), go->getY(), go->getZ(), go->getOrientation()); } } - // Track online item objects - if (block.objectType == ObjectType::ITEM) { + // Track online item objects (CONTAINER = bags, also tracked as items) + if (block.objectType == ObjectType::ITEM || block.objectType == ObjectType::CONTAINER) { auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT)); if (entryIt != block.fields.end() && entryIt->second != 0) { @@ -3172,6 +3172,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { onlineItems_[block.guid] = info; queryItemInfo(info.entry, block.guid); } + // Extract container slot GUIDs for bags + if (block.objectType == ObjectType::CONTAINER) { + extractContainerFields(block.guid, block.fields); + } } // Extract XP / inventory slot / skill fields for player entity @@ -3550,13 +3554,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } // Update item stack count for online items - if (entity->getType() == ObjectType::ITEM) { + if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) { for (const auto& [key, val] : block.fields) { if (key == fieldIndex(UF::ITEM_FIELD_STACK_COUNT)) { auto it = onlineItems_.find(block.guid); if (it != onlineItems_.end()) it->second.stackCount = val; } } + // Update container slot GUIDs on bag content changes + if (entity->getType() == ObjectType::CONTAINER) { + extractContainerFields(block.guid, block.fields); + } rebuildOnlineInventory(); } if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) { @@ -3773,7 +3781,8 @@ void GameHandler::handleDestroyObject(network::Packet& packet) { } hostileAttackers_.erase(data.guid); - // Remove online item tracking + // Remove online item/container tracking + containerContents_.erase(data.guid); if (onlineItems_.erase(data.guid)) { rebuildOnlineInventory(); } @@ -5264,6 +5273,32 @@ bool GameHandler::applyInventoryFields(const std::map& field return slotsChanged; } +void GameHandler::extractContainerFields(uint64_t containerGuid, const std::map& fields) { + const uint16_t numSlotsIdx = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS); + const uint16_t slot1Idx = fieldIndex(UF::CONTAINER_FIELD_SLOT_1); + if (numSlotsIdx == 0xFFFF || slot1Idx == 0xFFFF) return; + + auto& info = containerContents_[containerGuid]; + + // Read number of slots + auto numIt = fields.find(numSlotsIdx); + if (numIt != fields.end()) { + info.numSlots = std::min(numIt->second, 36u); + } + + // Read slot GUIDs (each is 2 uint32 fields: lo + hi) + for (const auto& [key, val] : fields) { + if (key < slot1Idx) continue; + int offset = key - slot1Idx; + int slotIndex = offset / 2; + if (slotIndex >= 36) continue; + bool isLow = (offset % 2 == 0); + uint64_t& guid = info.slotGuids[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + } +} + void GameHandler::rebuildOnlineInventory() { inventory = Inventory(); @@ -5338,6 +5373,78 @@ void GameHandler::rebuildOnlineInventory() { inventory.setBackpackSlot(i, def); } + // Bag contents (BAG1-BAG4 are equip slots 19-22) + for (int bagIdx = 0; bagIdx < 4; bagIdx++) { + uint64_t bagGuid = equipSlotGuids_[19 + bagIdx]; + if (bagGuid == 0) continue; + + // Determine bag size from container fields or item template + int numSlots = 0; + auto contIt = containerContents_.find(bagGuid); + if (contIt != containerContents_.end()) { + numSlots = static_cast(contIt->second.numSlots); + } + if (numSlots <= 0) { + auto bagItemIt = onlineItems_.find(bagGuid); + if (bagItemIt != onlineItems_.end()) { + auto bagInfoIt = itemInfoCache_.find(bagItemIt->second.entry); + if (bagInfoIt != itemInfoCache_.end()) { + numSlots = bagInfoIt->second.containerSlots; + } + } + } + if (numSlots <= 0) continue; + + // Set the bag size in the inventory bag data + inventory.setBagSize(bagIdx, numSlots); + + // Also set bagSlots on the equipped bag item (for UI display) + auto& bagEquipSlot = inventory.getEquipSlot(static_cast(19 + bagIdx)); + if (!bagEquipSlot.empty()) { + ItemDef bagDef = bagEquipSlot.item; + bagDef.bagSlots = numSlots; + inventory.setEquipSlot(static_cast(19 + bagIdx), bagDef); + } + + // Populate bag slot items + if (contIt == containerContents_.end()) continue; + const auto& container = contIt->second; + for (int s = 0; s < numSlots && s < 36; s++) { + uint64_t itemGuid = container.slotGuids[s]; + if (itemGuid == 0) continue; + + auto itemIt = onlineItems_.find(itemGuid); + 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; + def.bagSlots = infoIt->second.containerSlots; + } else { + def.name = "Item " + std::to_string(def.itemId); + queryItemInfo(def.itemId, itemGuid); + } + + inventory.setBagSlot(bagIdx, s, def); + } + } + onlineEquipDirty_ = true; LOG_DEBUG("Rebuilt online inventory: equip=", [&](){ @@ -6090,7 +6197,9 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { } uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid; - auto packet = CastSpellPacket::build(spellId, target, ++castCount); + auto packet = packetParsers_ + ? packetParsers_->buildCastSpell(spellId, target, ++castCount) + : CastSpellPacket::build(spellId, target, ++castCount); socket->send(packet); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); } diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index 81de074b..a7e7c6f2 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -50,6 +50,11 @@ int Inventory::getBagSize(int bagIndex) const { return bags[bagIndex].size; } +void Inventory::setBagSize(int bagIndex, int size) { + if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return; + bags[bagIndex].size = std::min(size, MAX_BAG_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; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 6b129887..f17c6cac 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -257,6 +257,44 @@ network::Packet ClassicPacketParsers::buildMovementPacket(LogicalOpcode opcode, return packet; } +// ============================================================================ +// Classic buildCastSpell +// Vanilla 1.12.x: NO castCount prefix, NO castFlags byte +// Format: uint32 spellId + uint32 targetFlags + [PackedGuid if unit target] +// ============================================================================ +network::Packet ClassicPacketParsers::buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t /*castCount*/) { + network::Packet packet(wireOpcode(LogicalOpcode::CMSG_CAST_SPELL)); + + packet.writeUInt32(spellId); + + // SpellCastTargets — vanilla uses uint16 target mask (not uint32 like WotLK) + if (targetGuid != 0) { + packet.writeUInt16(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.writeUInt16(0x00); // TARGET_FLAG_SELF + } + + return packet; +} + // ============================================================================ // Classic 1.12.1 parseCharEnum // Differences from TBC: diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index e219c524..6114fce8 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -48,6 +48,8 @@ static const UFNameEntry kUFNames[] = { {"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START}, {"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID}, {"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT}, + {"CONTAINER_FIELD_NUM_SLOTS", UF::CONTAINER_FIELD_NUM_SLOTS}, + {"CONTAINER_FIELD_SLOT_1", UF::CONTAINER_FIELD_SLOT_1}, }; static constexpr size_t kUFNameCount = sizeof(kUFNames) / sizeof(kUFNames[0]); @@ -85,6 +87,8 @@ void UpdateFieldTable::loadWotlkDefaults() { {UF::PLAYER_EXPLORED_ZONES_START, 1041}, {UF::GAMEOBJECT_DISPLAYID, 8}, {UF::ITEM_FIELD_STACK_COUNT, 14}, + {UF::CONTAINER_FIELD_NUM_SLOTS, 64}, // ITEM_END + 0 for WotLK + {UF::CONTAINER_FIELD_SLOT_1, 66}, // ITEM_END + 2 for WotLK }; for (auto& d : defaults) { fieldMap_[static_cast(d.field)] = d.idx;