diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index fa4b9ada..67019c80 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -33,6 +33,8 @@ "PLAYER_EXPLORED_ZONES_START": 1041, "GAMEOBJECT_DISPLAYID": 8, "ITEM_FIELD_STACK_COUNT": 14, + "ITEM_FIELD_DURABILITY": 60, + "ITEM_FIELD_MAXDURABILITY": 61, "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 f35304c6..2307f849 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1258,6 +1258,8 @@ public: uint32_t count = 1; }; void buyBackItem(uint32_t buybackSlot); + void repairItem(uint64_t vendorGuid, uint64_t itemGuid); + void repairAll(uint64_t vendorGuid, bool useGuildBank = false); const std::deque& getBuybackItems() const { return buybackItems_; } void autoEquipItemBySlot(int backpackIndex); void autoEquipItemInBag(int bagIndex, int slotIndex); @@ -1269,6 +1271,7 @@ public: void useItemById(uint32_t itemId); bool isVendorWindowOpen() const { return vendorWindowOpen; } const ListInventoryData& getVendorItems() const { return currentVendorItems; } + void setVendorCanRepair(bool v) { currentVendorItems.canRepair = v; } // Mail bool isMailboxOpen() const { return mailboxOpen_; } @@ -1831,6 +1834,8 @@ private: struct OnlineItemInfo { uint32_t entry = 0; uint32_t stackCount = 1; + uint32_t curDurability = 0; + uint32_t maxDurability = 0; }; std::unordered_map onlineItems_; std::unordered_map itemInfoCache_; diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index b25d5234..8dcd4ce2 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -46,6 +46,8 @@ struct ItemDef { int32_t spirit = 0; uint32_t displayInfoId = 0; uint32_t sellPrice = 0; + uint32_t curDurability = 0; + uint32_t maxDurability = 0; }; struct ItemSlot { diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index fd208554..67651b00 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -56,6 +56,8 @@ enum class UF : uint16_t { // Item fields ITEM_FIELD_STACK_COUNT, + ITEM_FIELD_DURABILITY, + ITEM_FIELD_MAXDURABILITY, // Container fields CONTAINER_FIELD_NUM_SLOTS, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 771bbda8..eaf1fd2f 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2179,6 +2179,7 @@ struct VendorItem { struct ListInventoryData { uint64_t vendorGuid = 0; std::vector items; + bool canRepair = false; // Set when vendor was opened via GOSSIP_OPTION_ARMORER bool isValid() const { return true; } }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 320a79c8..35ebca5f 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1426,17 +1426,20 @@ void Application::update(float deltaTime) { } } - // Use getLatestX/Y/Z (server-authoritative destination) for position sync - // rather than getX/Y/Z (interpolated), which may be stale for entities - // outside the 150-unit updateMovement() culling radius in GameHandler. - glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); + // Distance check uses getLatestX/Y/Z (server-authoritative destination) to + // avoid false-culling entities that moved while getX/Y/Z was stale. + // Position sync still uses getX/Y/Z to preserve smooth interpolation for + // nearby entities; distant entities (> 150u) have planarDist≈0 anyway + // so the renderer remains driven correctly by creatureMoveCallback_. + glm::vec3 latestCanonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); float canonDistSq = 0.0f; if (havePlayerPos) { - glm::vec3 d = canonical - playerPos; + glm::vec3 d = latestCanonical - playerPos; canonDistSq = glm::dot(d, d); if (canonDistSq > syncRadiusSq) continue; } + glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); glm::vec3 renderPos = core::coords::canonicalToRender(canonical); // Visual collision guard: keep hostile melee units from rendering inside the diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index dbdf296c..a79ab584 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8023,10 +8023,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { 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)); + auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY)); + auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY)); if (entryIt != block.fields.end() && entryIt->second != 0) { - OnlineItemInfo info; + // Preserve existing info when doing partial updates + OnlineItemInfo info = onlineItems_.count(block.guid) + ? onlineItems_[block.guid] : OnlineItemInfo{}; info.entry = entryIt->second; - info.stackCount = (stackIt != block.fields.end()) ? stackIt->second : 1; + if (stackIt != block.fields.end()) info.stackCount = stackIt->second; + if (durIt != block.fields.end()) info.curDurability = durIt->second; + if (maxDurIt!= block.fields.end()) info.maxDurability = maxDurIt->second; bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end()); onlineItems_[block.guid] = info; if (isNew) newItemCreated = true; @@ -8427,19 +8433,31 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { extractExploredZoneFields(lastPlayerFields_); } - // Update item stack count for online items + // Update item stack count / durability for online items if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) { bool inventoryChanged = false; - const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT); + const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT); + const uint16_t itemDurField = fieldIndex(UF::ITEM_FIELD_DURABILITY); + const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY); const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS); const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1); for (const auto& [key, val] : block.fields) { + auto it = onlineItems_.find(block.guid); if (key == itemStackField) { - auto it = onlineItems_.find(block.guid); if (it != onlineItems_.end() && it->second.stackCount != val) { it->second.stackCount = val; inventoryChanged = true; } + } else if (key == itemDurField) { + if (it != onlineItems_.end() && it->second.curDurability != val) { + it->second.curDurability = val; + inventoryChanged = true; + } + } else if (key == itemMaxDurField) { + if (it != onlineItems_.end() && it->second.maxDurability != val) { + it->second.maxDurability = val; + inventoryChanged = true; + } } } // Update container slot GUIDs on bag content changes @@ -10719,6 +10737,8 @@ void GameHandler::rebuildOnlineInventory() { ItemDef def; def.itemId = itemIt->second.entry; def.stackCount = itemIt->second.stackCount; + def.curDurability = itemIt->second.curDurability; + def.maxDurability = itemIt->second.maxDurability; def.maxStack = 1; auto infoIt = itemInfoCache_.find(itemIt->second.entry); @@ -10757,6 +10777,8 @@ void GameHandler::rebuildOnlineInventory() { ItemDef def; def.itemId = itemIt->second.entry; def.stackCount = itemIt->second.stackCount; + def.curDurability = itemIt->second.curDurability; + def.maxDurability = itemIt->second.maxDurability; def.maxStack = 1; auto infoIt = itemInfoCache_.find(itemIt->second.entry); @@ -10830,6 +10852,8 @@ void GameHandler::rebuildOnlineInventory() { ItemDef def; def.itemId = itemIt->second.entry; def.stackCount = itemIt->second.stackCount; + def.curDurability = itemIt->second.curDurability; + def.maxDurability = itemIt->second.maxDurability; def.maxStack = 1; auto infoIt = itemInfoCache_.find(itemIt->second.entry); @@ -10870,6 +10894,8 @@ void GameHandler::rebuildOnlineInventory() { ItemDef def; def.itemId = itemIt->second.entry; def.stackCount = itemIt->second.stackCount; + def.curDurability = itemIt->second.curDurability; + def.maxDurability = itemIt->second.maxDurability; def.maxStack = 1; auto infoIt = itemInfoCache_.find(itemIt->second.entry); @@ -10951,6 +10977,8 @@ void GameHandler::rebuildOnlineInventory() { ItemDef def; def.itemId = itemIt->second.entry; def.stackCount = itemIt->second.stackCount; + def.curDurability = itemIt->second.curDurability; + def.maxDurability = itemIt->second.maxDurability; def.maxStack = 1; auto infoIt = itemInfoCache_.find(itemIt->second.entry); @@ -14866,6 +14894,26 @@ void GameHandler::buyBackItem(uint32_t buybackSlot) { socket->send(packet); } +void GameHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) { + if (state != WorldState::IN_WORLD || !socket) return; + // CMSG_REPAIR_ITEM: npcGuid(8) + itemGuid(8) + useGuildBank(uint8) + network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM)); + packet.writeUInt64(vendorGuid); + packet.writeUInt64(itemGuid); + packet.writeUInt8(0); // do not use guild bank + socket->send(packet); +} + +void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) { + if (state != WorldState::IN_WORLD || !socket) return; + // itemGuid = 0 signals "repair all equipped" to the server + network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM)); + packet.writeUInt64(vendorGuid); + packet.writeUInt64(0); + packet.writeUInt8(useGuildBank ? 1 : 0); + socket->send(packet); +} + void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) { if (state != WorldState::IN_WORLD || !socket) return; LOG_INFO("Sell request: vendorGuid=0x", std::hex, vendorGuid, diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 41ea69a3..cf924c08 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -6538,6 +6538,9 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { std::string processedText = replaceGenderPlaceholders(displayText, gameHandler); std::string label = std::string(icon) + " " + processedText; if (ImGui::Selectable(label.c_str())) { + if (opt.text == "GOSSIP_OPTION_ARMORER") { + gameHandler.setVendorCanRepair(true); + } gameHandler.selectGossipOption(opt.id); } ImGui::PopID(); @@ -6936,6 +6939,17 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); + + if (vendor.canRepair) { + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f); + if (ImGui::SmallButton("Repair All")) { + gameHandler.repairAll(vendor.vendorGuid, false); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Repair all equipped items"); + } + } ImGui::Separator(); ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell"); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 320fc316..edf18525 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1805,6 +1805,15 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I if (!bonusLine.empty()) { ImGui::TextColored(green, "%s", bonusLine.c_str()); } + if (item.maxDurability > 0) { + float durPct = static_cast(item.curDurability) / static_cast(item.maxDurability); + ImVec4 durColor; + if (durPct > 0.5f) durColor = ImVec4(0.1f, 1.0f, 0.1f, 1.0f); // green + else if (durPct > 0.25f) durColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // yellow + else durColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); // red + ImGui::TextColored(durColor, "Durability %u / %u", + item.curDurability, item.maxDurability); + } if (item.sellPrice > 0) { uint32_t g = item.sellPrice / 10000; uint32_t s = (item.sellPrice / 100) % 100;