mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix weapon attachments and inspect fallback
This commit is contained in:
parent
2774b47867
commit
60c93fa1e3
3 changed files with 183 additions and 3 deletions
|
|
@ -837,6 +837,7 @@ private:
|
||||||
void handleCreatureQueryResponse(network::Packet& packet);
|
void handleCreatureQueryResponse(network::Packet& packet);
|
||||||
void handleGameObjectQueryResponse(network::Packet& packet);
|
void handleGameObjectQueryResponse(network::Packet& packet);
|
||||||
void handleItemQueryResponse(network::Packet& packet);
|
void handleItemQueryResponse(network::Packet& packet);
|
||||||
|
void handleInspectResults(network::Packet& packet);
|
||||||
void queryItemInfo(uint32_t entry, uint64_t guid);
|
void queryItemInfo(uint32_t entry, uint64_t guid);
|
||||||
void rebuildOnlineInventory();
|
void rebuildOnlineInventory();
|
||||||
void maybeDetectVisibleItemLayout();
|
void maybeDetectVisibleItemLayout();
|
||||||
|
|
@ -1085,6 +1086,11 @@ private:
|
||||||
std::unordered_set<uint64_t> otherPlayerVisibleDirty_;
|
std::unordered_set<uint64_t> otherPlayerVisibleDirty_;
|
||||||
std::unordered_map<uint64_t, uint32_t> otherPlayerMoveTimeMs_;
|
std::unordered_map<uint64_t, uint32_t> otherPlayerMoveTimeMs_;
|
||||||
|
|
||||||
|
// Inspect fallback (when visible item fields are missing/unreliable)
|
||||||
|
std::unordered_map<uint64_t, std::array<uint32_t, 19>> inspectedPlayerItemEntries_;
|
||||||
|
std::unordered_set<uint64_t> pendingAutoInspect_;
|
||||||
|
float inspectRateLimit_ = 0.0f;
|
||||||
|
|
||||||
// ---- Phase 2: Combat ----
|
// ---- Phase 2: Combat ----
|
||||||
bool autoAttacking = false;
|
bool autoAttacking = false;
|
||||||
uint64_t autoAttackTarget = 0;
|
uint64_t autoAttackTarget = 0;
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Send periodic heartbeat if in world
|
||||||
if (state == WorldState::IN_WORLD) {
|
if (state == WorldState::IN_WORLD) {
|
||||||
timeSinceLastPing += deltaTime;
|
timeSinceLastPing += deltaTime;
|
||||||
|
|
@ -760,6 +774,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleItemQueryResponse(packet);
|
handleItemQueryResponse(packet);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Opcode::SMSG_INSPECT_RESULTS:
|
||||||
|
handleInspectResults(packet);
|
||||||
|
break;
|
||||||
|
|
||||||
// ---- XP ----
|
// ---- XP ----
|
||||||
case Opcode::SMSG_LOG_XPGAIN:
|
case Opcode::SMSG_LOG_XPGAIN:
|
||||||
handleXpGain(packet);
|
handleXpGain(packet);
|
||||||
|
|
@ -2847,6 +2865,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
otherPlayerVisibleItemEntries_.erase(guid);
|
otherPlayerVisibleItemEntries_.erase(guid);
|
||||||
otherPlayerVisibleDirty_.erase(guid);
|
otherPlayerVisibleDirty_.erase(guid);
|
||||||
otherPlayerMoveTimeMs_.erase(guid);
|
otherPlayerMoveTimeMs_.erase(guid);
|
||||||
|
inspectedPlayerItemEntries_.erase(guid);
|
||||||
|
pendingAutoInspect_.erase(guid);
|
||||||
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
||||||
gameObjectDespawnCallback_(guid);
|
gameObjectDespawnCallback_(guid);
|
||||||
}
|
}
|
||||||
|
|
@ -3263,6 +3283,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
if (applyInventoryFields(block.fields)) slotsChanged = true;
|
||||||
if (slotsChanged) rebuildOnlineInventory();
|
if (slotsChanged) rebuildOnlineInventory();
|
||||||
|
maybeDetectVisibleItemLayout();
|
||||||
extractSkillFields(lastPlayerFields_);
|
extractSkillFields(lastPlayerFields_);
|
||||||
extractExploredZoneFields(lastPlayerFields_);
|
extractExploredZoneFields(lastPlayerFields_);
|
||||||
}
|
}
|
||||||
|
|
@ -4991,6 +5012,95 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) {
|
||||||
rebuildOnlineInventory();
|
rebuildOnlineInventory();
|
||||||
maybeDetectVisibleItemLayout();
|
maybeDetectVisibleItemLayout();
|
||||||
emitAllOtherPlayerEquipment();
|
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<uint32_t, 19> displayIds{};
|
||||||
|
std::array<uint8_t, 19> 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<uint8_t>(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<std::array<uint32_t, 19>> {
|
||||||
|
if (remaining < 19 * perSlotBytes) return std::nullopt;
|
||||||
|
auto saved = packet.getReadPos();
|
||||||
|
std::array<uint32_t, 19> 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<std::array<uint32_t, 19>> 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<uint32_t, 19> displayIds{};
|
||||||
|
std::array<uint8_t, 19> 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<uint8_t>(infoIt->second.inventoryType);
|
||||||
|
}
|
||||||
|
playerEquipmentCallback_(guid, displayIds, invTypes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5233,6 +5343,8 @@ void GameHandler::maybeDetectVisibleItemLayout() {
|
||||||
int bestBase = -1;
|
int bestBase = -1;
|
||||||
int bestStride = 0;
|
int bestStride = 0;
|
||||||
int bestMatches = 0;
|
int bestMatches = 0;
|
||||||
|
int bestMismatches = 9999;
|
||||||
|
int bestScore = -999999;
|
||||||
|
|
||||||
const int strides[] = {2, 3, 4, 1};
|
const int strides[] = {2, 3, 4, 1};
|
||||||
for (int stride : strides) {
|
for (int stride : strides) {
|
||||||
|
|
@ -5241,16 +5353,28 @@ void GameHandler::maybeDetectVisibleItemLayout() {
|
||||||
if (base + 18 * stride > static_cast<int>(maxKey)) continue;
|
if (base + 18 * stride > static_cast<int>(maxKey)) continue;
|
||||||
|
|
||||||
int matches = 0;
|
int matches = 0;
|
||||||
|
int mismatches = 0;
|
||||||
for (int s = 0; s < 19; s++) {
|
for (int s = 0; s < 19; s++) {
|
||||||
uint32_t want = equipEntries[s];
|
uint32_t want = equipEntries[s];
|
||||||
if (want == 0) continue;
|
if (want == 0) continue;
|
||||||
const uint16_t idx = static_cast<uint16_t>(base + s * stride);
|
const uint16_t idx = static_cast<uint16_t>(base + s * stride);
|
||||||
auto it = lastPlayerFields_.find(idx);
|
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;
|
bestMatches = matches;
|
||||||
|
bestMismatches = mismatches;
|
||||||
bestBase = base;
|
bestBase = base;
|
||||||
bestStride = stride;
|
bestStride = stride;
|
||||||
}
|
}
|
||||||
|
|
@ -5258,11 +5382,13 @@ void GameHandler::maybeDetectVisibleItemLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestMatches < 2 || bestBase < 0 || bestStride <= 0) return;
|
if (bestMatches < 2 || bestBase < 0 || bestStride <= 0) return;
|
||||||
|
if (bestMismatches > 1) return;
|
||||||
|
|
||||||
visibleItemEntryBase_ = bestBase;
|
visibleItemEntryBase_ = bestBase;
|
||||||
visibleItemStride_ = bestStride;
|
visibleItemStride_ = bestStride;
|
||||||
LOG_INFO("Detected PLAYER_VISIBLE_ITEM entry layout: base=", visibleItemEntryBase_,
|
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.
|
// Backfill existing player entities already in view.
|
||||||
for (const auto& [guid, ent] : entityManager.getEntities()) {
|
for (const auto& [guid, ent] : entityManager.getEntities()) {
|
||||||
|
|
@ -5298,6 +5424,13 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map<ui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the server isn't sending visible item fields (all zeros), fall back to inspect.
|
||||||
|
bool any = false;
|
||||||
|
for (uint32_t e : newEntries) { if (e != 0) { any = true; break; } }
|
||||||
|
if (!any && socket && state == WorldState::IN_WORLD) {
|
||||||
|
pendingAutoInspect_.insert(guid);
|
||||||
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
otherPlayerVisibleDirty_.insert(guid);
|
otherPlayerVisibleDirty_.insert(guid);
|
||||||
emitOtherPlayerEquipment(guid);
|
emitOtherPlayerEquipment(guid);
|
||||||
|
|
@ -5311,10 +5444,12 @@ void GameHandler::emitOtherPlayerEquipment(uint64_t guid) {
|
||||||
|
|
||||||
std::array<uint32_t, 19> displayIds{};
|
std::array<uint32_t, 19> displayIds{};
|
||||||
std::array<uint8_t, 19> invTypes{};
|
std::array<uint8_t, 19> invTypes{};
|
||||||
|
bool anyEntry = false;
|
||||||
|
|
||||||
for (int s = 0; s < 19; s++) {
|
for (int s = 0; s < 19; s++) {
|
||||||
uint32_t entry = it->second[s];
|
uint32_t entry = it->second[s];
|
||||||
if (entry == 0) continue;
|
if (entry == 0) continue;
|
||||||
|
anyEntry = true;
|
||||||
auto infoIt = itemInfoCache_.find(entry);
|
auto infoIt = itemInfoCache_.find(entry);
|
||||||
if (infoIt == itemInfoCache_.end()) continue;
|
if (infoIt == itemInfoCache_.end()) continue;
|
||||||
displayIds[s] = infoIt->second.displayInfoId;
|
displayIds[s] = infoIt->second.displayInfoId;
|
||||||
|
|
@ -5323,6 +5458,13 @@ void GameHandler::emitOtherPlayerEquipment(uint64_t guid) {
|
||||||
|
|
||||||
playerEquipmentCallback_(guid, displayIds, invTypes);
|
playerEquipmentCallback_(guid, displayIds, invTypes);
|
||||||
otherPlayerVisibleDirty_.erase(guid);
|
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() {
|
void GameHandler::emitAllOtherPlayerEquipment() {
|
||||||
|
|
|
||||||
|
|
@ -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<uint16_t>(i);
|
||||||
|
offset = glm::vec3(0.0f);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
core::Logger::getInstance().warning("attachWeapon: no bone found for attachment ", attachmentId);
|
core::Logger::getInstance().warning("attachWeapon: no bone found for attachment ", attachmentId);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1916,6 +1932,22 @@ bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t att
|
||||||
|
|
||||||
if (!found) return false;
|
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<uint16_t>(i);
|
||||||
|
offset = glm::vec3(0.0f);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Get bone matrix
|
// Get bone matrix
|
||||||
glm::mat4 boneMat(1.0f);
|
glm::mat4 boneMat(1.0f);
|
||||||
if (boneIndex < instance.boneMatrices.size()) {
|
if (boneIndex < instance.boneMatrices.size()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue