From 60c93fa1e3348044172d97d707aad6a0797efbe7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Feb 2026 20:26:55 -0800 Subject: [PATCH] Fix weapon attachments and inspect fallback --- include/game/game_handler.hpp | 6 ++ src/game/game_handler.cpp | 148 ++++++++++++++++++++++++++- src/rendering/character_renderer.cpp | 32 ++++++ 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 98a880f0..b84b948f 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -837,6 +837,7 @@ private: void handleCreatureQueryResponse(network::Packet& packet); void handleGameObjectQueryResponse(network::Packet& packet); void handleItemQueryResponse(network::Packet& packet); + void handleInspectResults(network::Packet& packet); void queryItemInfo(uint32_t entry, uint64_t guid); void rebuildOnlineInventory(); void maybeDetectVisibleItemLayout(); @@ -1085,6 +1086,11 @@ private: std::unordered_set otherPlayerVisibleDirty_; std::unordered_map otherPlayerMoveTimeMs_; + // Inspect fallback (when visible item fields are missing/unreliable) + std::unordered_map> inspectedPlayerItemEntries_; + std::unordered_set pendingAutoInspect_; + float inspectRateLimit_ = 0.0f; + // ---- Phase 2: Combat ---- bool autoAttacking = false; uint64_t autoAttackTarget = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index dda8b3ff..2a20d852 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -263,6 +263,20 @@ void GameHandler::update(float deltaTime) { } } + // Auto-inspect throttling (fallback for player equipment visuals). + if (inspectRateLimit_ > 0.0f) { + inspectRateLimit_ = std::max(0.0f, inspectRateLimit_ - deltaTime); + } + if (state == WorldState::IN_WORLD && socket && inspectRateLimit_ <= 0.0f && !pendingAutoInspect_.empty()) { + uint64_t guid = *pendingAutoInspect_.begin(); + pendingAutoInspect_.erase(pendingAutoInspect_.begin()); + if (guid != 0 && guid != playerGuid && entityManager.hasEntity(guid)) { + auto pkt = InspectPacket::build(guid); + socket->send(pkt); + inspectRateLimit_ = 0.75f; // keep it gentle + } + } + // Send periodic heartbeat if in world if (state == WorldState::IN_WORLD) { timeSinceLastPing += deltaTime; @@ -760,6 +774,10 @@ void GameHandler::handlePacket(network::Packet& packet) { handleItemQueryResponse(packet); break; + case Opcode::SMSG_INSPECT_RESULTS: + handleInspectResults(packet); + break; + // ---- XP ---- case Opcode::SMSG_LOG_XPGAIN: handleXpGain(packet); @@ -2847,6 +2865,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { otherPlayerVisibleItemEntries_.erase(guid); otherPlayerVisibleDirty_.erase(guid); otherPlayerMoveTimeMs_.erase(guid); + inspectedPlayerItemEntries_.erase(guid); + pendingAutoInspect_.erase(guid); } else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) { gameObjectDespawnCallback_(guid); } @@ -3263,6 +3283,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); + maybeDetectVisibleItemLayout(); extractSkillFields(lastPlayerFields_); extractExploredZoneFields(lastPlayerFields_); } @@ -4991,6 +5012,95 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) { rebuildOnlineInventory(); maybeDetectVisibleItemLayout(); emitAllOtherPlayerEquipment(); + // If we have inspect-based item entry lists, re-emit for any players that now resolve. + if (playerEquipmentCallback_) { + for (const auto& [guid, entries] : inspectedPlayerItemEntries_) { + std::array displayIds{}; + std::array invTypes{}; + for (int s = 0; s < 19; s++) { + uint32_t entry = entries[s]; + if (entry == 0) continue; + auto infoIt = itemInfoCache_.find(entry); + if (infoIt == itemInfoCache_.end()) continue; + displayIds[s] = infoIt->second.displayInfoId; + invTypes[s] = static_cast(infoIt->second.inventoryType); + } + playerEquipmentCallback_(guid, displayIds, invTypes); + } + } + } +} + +void GameHandler::handleInspectResults(network::Packet& packet) { + // Best-effort parsing across Classic/TBC/WotLK variants. + // We only care about item entry IDs per equip slot. + if (packet.getSize() - packet.getReadPos() < 8) return; + + uint64_t guid = packet.readUInt64(); + if (guid == 0) return; + + const size_t remaining = packet.getSize() - packet.getReadPos(); + + auto tryParseFixed = [&](size_t perSlotBytes, size_t itemIdOffset) -> std::optional> { + if (remaining < 19 * perSlotBytes) return std::nullopt; + auto saved = packet.getReadPos(); + std::array items{}; + bool plausible = false; + + for (int i = 0; i < 19; i++) { + if (perSlotBytes == 4) { + items[i] = packet.readUInt32(); + } else if (perSlotBytes == 8) { + uint32_t a = packet.readUInt32(); + uint32_t b = packet.readUInt32(); + items[i] = (itemIdOffset == 0) ? a : b; + } else { + packet.setReadPos(saved); + return std::nullopt; + } + if (items[i] > 0 && items[i] < 5000000u) plausible = true; + } + + // Rewind to allow other attempts if implausible. + if (!plausible) { + packet.setReadPos(saved); + return std::nullopt; + } + return items; + }; + + std::optional> parsed; + // Common shapes: [guid][19*uint32 itemId] or [guid][19*(uint32 itemId, uint32 enchant)]. + parsed = tryParseFixed(4, 0); + if (!parsed) parsed = tryParseFixed(8, 0); + if (!parsed) parsed = tryParseFixed(8, 4); // sometimes itemId is second dword + + if (!parsed) { + LOG_WARNING("SMSG_INSPECT_RESULTS: unrecognized payload size=", remaining, " for guid=0x", std::hex, guid, std::dec); + return; + } + + inspectedPlayerItemEntries_[guid] = *parsed; + + // Query item templates so we can resolve displayInfoId/inventoryType. + for (uint32_t entry : *parsed) { + if (entry == 0) continue; + queryItemInfo(entry, 0); + } + + // If templates already exist, emit immediately. + if (playerEquipmentCallback_) { + std::array displayIds{}; + std::array invTypes{}; + for (int s = 0; s < 19; s++) { + uint32_t entry = (*parsed)[s]; + if (entry == 0) continue; + auto infoIt = itemInfoCache_.find(entry); + if (infoIt == itemInfoCache_.end()) continue; + displayIds[s] = infoIt->second.displayInfoId; + invTypes[s] = static_cast(infoIt->second.inventoryType); + } + playerEquipmentCallback_(guid, displayIds, invTypes); } } @@ -5233,6 +5343,8 @@ void GameHandler::maybeDetectVisibleItemLayout() { int bestBase = -1; int bestStride = 0; int bestMatches = 0; + int bestMismatches = 9999; + int bestScore = -999999; const int strides[] = {2, 3, 4, 1}; for (int stride : strides) { @@ -5241,16 +5353,28 @@ void GameHandler::maybeDetectVisibleItemLayout() { if (base + 18 * stride > static_cast(maxKey)) continue; int matches = 0; + int mismatches = 0; for (int s = 0; s < 19; s++) { uint32_t want = equipEntries[s]; if (want == 0) continue; const uint16_t idx = static_cast(base + s * stride); auto it = lastPlayerFields_.find(idx); - if (it != lastPlayerFields_.end() && it->second == want) matches++; + if (it == lastPlayerFields_.end()) continue; + if (it->second == want) { + matches++; + } else if (it->second != 0) { + mismatches++; + } } - if (matches > bestMatches || (matches == bestMatches && matches > 0 && base < bestBase)) { + int score = matches * 2 - mismatches * 3; + if (score > bestScore || + (score == bestScore && matches > bestMatches) || + (score == bestScore && matches == bestMatches && mismatches < bestMismatches) || + (score == bestScore && matches == bestMatches && mismatches == bestMismatches && base < bestBase)) { + bestScore = score; bestMatches = matches; + bestMismatches = mismatches; bestBase = base; bestStride = stride; } @@ -5258,11 +5382,13 @@ void GameHandler::maybeDetectVisibleItemLayout() { } if (bestMatches < 2 || bestBase < 0 || bestStride <= 0) return; + if (bestMismatches > 1) return; visibleItemEntryBase_ = bestBase; visibleItemStride_ = bestStride; LOG_INFO("Detected PLAYER_VISIBLE_ITEM entry layout: base=", visibleItemEntryBase_, - " stride=", visibleItemStride_, " (matches=", bestMatches, ")"); + " stride=", visibleItemStride_, " (matches=", bestMatches, + " mismatches=", bestMismatches, " score=", bestScore, ")"); // Backfill existing player entities already in view. for (const auto& [guid, ent] : entityManager.getEntities()) { @@ -5298,6 +5424,13 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map displayIds{}; std::array invTypes{}; + bool anyEntry = false; for (int s = 0; s < 19; s++) { uint32_t entry = it->second[s]; if (entry == 0) continue; + anyEntry = true; auto infoIt = itemInfoCache_.find(entry); if (infoIt == itemInfoCache_.end()) continue; displayIds[s] = infoIt->second.displayInfoId; @@ -5323,6 +5458,13 @@ void GameHandler::emitOtherPlayerEquipment(uint64_t guid) { playerEquipmentCallback_(guid, displayIds, invTypes); otherPlayerVisibleDirty_.erase(guid); + + // If we had entries but couldn't resolve any templates, also try inspect as a fallback. + bool anyResolved = false; + for (uint32_t did : displayIds) { if (did != 0) { anyResolved = true; break; } } + if (anyEntry && !anyResolved) { + pendingAutoInspect_.insert(guid); + } } void GameHandler::emitAllOtherPlayerEquipment() { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 7f87a18b..d5acf60f 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1792,6 +1792,22 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen } } + // Validate bone index (bad attachment tables should not silently bind to origin) + if (found && boneIndex >= charModel.bones.size()) { + found = false; + } + if (!found) { + int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27; + for (size_t i = 0; i < charModel.bones.size(); i++) { + if (charModel.bones[i].keyBoneId == targetKeyBone) { + boneIndex = static_cast(i); + offset = glm::vec3(0.0f); + found = true; + break; + } + } + } + if (!found) { core::Logger::getInstance().warning("attachWeapon: no bone found for attachment ", attachmentId); return false; @@ -1916,6 +1932,22 @@ bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t att if (!found) return false; + // Validate bone index; invalid indices bind attachments to origin (looks like weapons at feet). + if (boneIndex >= model.bones.size()) { + // Fallback: key bones (26/27) for hand attachments. + int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27; + found = false; + for (size_t i = 0; i < model.bones.size(); i++) { + if (model.bones[i].keyBoneId == targetKeyBone) { + boneIndex = static_cast(i); + offset = glm::vec3(0.0f); + found = true; + break; + } + } + if (!found) return false; + } + // Get bone matrix glm::mat4 boneMat(1.0f); if (boneIndex < instance.boneMatrices.size()) {