From 8a617e842bdbde0b417525963d82e21c7cab7170 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Mar 2026 17:41:42 -0700 Subject: [PATCH] fix: direct CMSG_LOOT for chest GOs and increase M2 descriptor pools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chest-type game objects (quest pickups, treasure chests) now send CMSG_LOOT directly with the GO GUID instead of CMSG_GAMEOBJ_USE + delayed CMSG_LOOT. The server's loot handler activates the GO and sends SMSG_LOOT_RESPONSE in one step. The old approach failed because CMSG_GAMEOBJ_USE opened+despawned the GO before CMSG_LOOT arrived. Double M2 bone and material descriptor pool sizes (8192 → 16384) to handle the increased NPC count from the spline parsing fix — patrolling NPCs that were previously invisible now spawn correctly, exhausting the old pool limits. --- include/rendering/m2_renderer.hpp | 4 +- src/game/game_handler.cpp | 100 ++++++++++++------------------ 2 files changed, 43 insertions(+), 61 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index c50dfb0f..fe8d7f61 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -413,8 +413,8 @@ private: // Descriptor pools VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE; VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE; - static constexpr uint32_t MAX_MATERIAL_SETS = 8192; - static constexpr uint32_t MAX_BONE_SETS = 8192; + static constexpr uint32_t MAX_MATERIAL_SETS = 16384; + static constexpr uint32_t MAX_BONE_SETS = 16384; // Dummy identity bone buffer + descriptor set for non-animated models. // The pipeline layout declares set 2 (bones) and some drivers (Intel ANV) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fb4b7bb8..e1f30ac0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -21306,41 +21306,14 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } - auto packet = GameObjectUsePacket::build(guid); - LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec, - " entry=", goEntry, " type=", goType, - " name='", goName, "' wireOp=0x", std::hex, packet.getOpcode(), std::dec, - " pktSize=", packet.getSize(), - " dist=", entity ? std::sqrt( - (entity->getX() - movementInfo.x) * (entity->getX() - movementInfo.x) + - (entity->getY() - movementInfo.y) * (entity->getY() - movementInfo.y) + - (entity->getZ() - movementInfo.z) * (entity->getZ() - movementInfo.z)) : -1.0f); - socket->send(packet); - lastInteractedGoGuid_ = guid; - - // For mailbox GameObjects (type 19), open mail UI and request mail list. - // In Vanilla/Classic there is no SMSG_SHOW_MAILBOX — the server just sends - // animation/sound and expects the client to request the mail list. + // Determine GO type for interaction strategy bool isMailbox = false; bool chestLike = false; - // Do NOT send CMSG_LOOT after CMSG_GAMEOBJ_USE — the server handles loot - // automatically as part of the USE handler. Sending a redundant CMSG_LOOT - // triggers DoLootRelease() on the server, which closes the loot the server - // just opened before the client ever sees SMSG_LOOT_RESPONSE. - bool shouldSendLoot = false; if (entity && entity->getType() == ObjectType::GAMEOBJECT) { auto go = std::static_pointer_cast(entity); auto* info = getCachedGameObjectInfo(go->getEntry()); if (info && info->type == 19) { isMailbox = true; - shouldSendLoot = false; - LOG_INFO("Mailbox interaction: opening mail UI and requesting mail list"); - mailboxGuid_ = guid; - mailboxOpen_ = true; - hasNewMail_ = false; - selectedMailIndex_ = -1; - showMailCompose_ = false; - refreshMailList(); } else if (info && info->type == 3) { chestLike = true; } @@ -21353,40 +21326,49 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { lower.find("lockbox") != std::string::npos || lower.find("strongbox") != std::string::npos || lower.find("coffer") != std::string::npos || - lower.find("cache") != std::string::npos); + lower.find("cache") != std::string::npos || + lower.find("bundle") != std::string::npos); } - // Some servers require CMSG_GAMEOBJ_REPORT_USE for lootable gameobjects. - // Only send it when the active opcode table actually supports it. - if (!isMailbox) { - const auto* table = getActiveOpcodeTable(); - if (table && table->hasOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)) { - network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); - reportUse.writeUInt64(guid); - socket->send(reportUse); + + LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec, + " entry=", goEntry, " type=", goType, + " name='", goName, "' chestLike=", chestLike, " isMailbox=", isMailbox); + + if (chestLike) { + // For chest-like GOs (quest objects, treasure chests, etc.), send CMSG_LOOT + // directly with the GO GUID — same as creature corpse looting. The server's + // LOOT handler activates the GO, generates loot, and sends SMSG_LOOT_RESPONSE + // in a single step. Sending CMSG_GAMEOBJ_USE + delayed CMSG_LOOT doesn't work + // because the USE handler opens+despawns the GO before CMSG_LOOT arrives, and + // CMSG_LOOT's DoLootRelease() closes any loot the USE handler already opened. + lootTarget(guid); + lastInteractedGoGuid_ = guid; + } else { + // Non-chest GOs (doors, buttons, quest givers, etc.): use CMSG_GAMEOBJ_USE + auto packet = GameObjectUsePacket::build(guid); + socket->send(packet); + lastInteractedGoGuid_ = guid; + + if (isMailbox) { + LOG_INFO("Mailbox interaction: opening mail UI and requesting mail list"); + mailboxGuid_ = guid; + mailboxOpen_ = true; + hasNewMail_ = false; + selectedMailIndex_ = -1; + showMailCompose_ = false; + refreshMailList(); + } + + // CMSG_GAMEOBJ_REPORT_USE for GO AI scripts (quest givers, etc.) + if (!isMailbox) { + const auto* table = getActiveOpcodeTable(); + if (table && table->hasOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)) { + network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); + reportUse.writeUInt64(guid); + socket->send(reportUse); + } } } - if (shouldSendLoot) { - // Don't send CMSG_LOOT immediately — give the server time to process - // CMSG_GAMEOBJ_USE first (chests need to transition to lootable state, - // gathering nodes start a spell cast). A premature CMSG_LOOT can cause - // an empty SMSG_LOOT_RESPONSE that clears our gather-cast loot state. - pendingGameObjectLootOpens_.erase( - std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(), - [&](const PendingLootOpen& p) { return p.guid == guid; }), - pendingGameObjectLootOpens_.end()); - // Short delay for chests (server makes them lootable quickly after USE), - // plus a longer retry to catch slow state transitions. - pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.20f}); - pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.75f}); - } else { - // Non-lootable interaction (mailbox, door, button, etc.) — no CMSG_LOOT will be - // sent, and no SMSG_LOOT_RESPONSE will arrive to clear it. Clear the gather-loot - // guid now so a subsequent timed cast completion can't fire a spurious CMSG_LOOT. - lastInteractedGoGuid_ = 0; - } - // Don't retry CMSG_GAMEOBJ_USE — resending can toggle chest state on some - // servers (opening→closing the chest). The delayed CMSG_LOOT retries above - // handle the case where the first loot attempt arrives too early. } void GameHandler::selectGossipOption(uint32_t optionId) {