diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 1d40d16c..31a60fb7 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1953,6 +1953,7 @@ public: void buyBackItem(uint32_t buybackSlot); void repairItem(uint64_t vendorGuid, uint64_t itemGuid); void repairAll(uint64_t vendorGuid, bool useGuildBank = false); + uint32_t estimateRepairAllCost() const; const std::deque& getBuybackItems() const; void autoEquipItemBySlot(int backpackIndex); void autoEquipItemInBag(int bagIndex, int slotIndex); diff --git a/include/game/inventory_handler.hpp b/include/game/inventory_handler.hpp index 15c5b5b7..d5e0239a 100644 --- a/include/game/inventory_handler.hpp +++ b/include/game/inventory_handler.hpp @@ -113,6 +113,7 @@ public: void buyBackItem(uint32_t buybackSlot); void repairItem(uint64_t vendorGuid, uint64_t itemGuid); void repairAll(uint64_t vendorGuid, bool useGuildBank = false); + uint32_t estimateRepairAllCost() const; const std::deque& getBuybackItems() const { return buybackItems_; } void autoEquipItemBySlot(int backpackIndex); void autoEquipItemInBag(int bagIndex, int slotIndex); @@ -397,6 +398,15 @@ private: std::string pendingSaveSetName_; std::string pendingSaveSetIcon_; std::vector equipmentSetInfo_; + + // ---- Repair cost DBC cache ---- + mutable bool repairDbcLoaded_ = false; + // DurabilityCosts.dbc: [itemLevel] -> multiplier[29] (weapon subclass 0-20, armor subclass+21) + mutable std::unordered_map> durabilityCosts_; + // DurabilityQuality.dbc: [id] -> quality_mod float + mutable std::unordered_map durabilityQuality_; + void loadRepairDbc() const; + uint32_t estimateItemRepairCost(uint64_t itemGuid) const; }; } // namespace game diff --git a/src/game/entity_controller.cpp b/src/game/entity_controller.cpp index c1cfe781..6eb3a307 100644 --- a/src/game/entity_controller.cpp +++ b/src/game/entity_controller.cpp @@ -1202,6 +1202,8 @@ void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block, const uint32_t prevDur = it->second.curDurability; it->second.curDurability = val; inventoryChanged = true; + LOG_DEBUG("Item durability update: guid=0x", std::hex, block.guid, + std::dec, " dur ", prevDur, "->", val, "/", it->second.maxDurability); // Warn once when durability drops below 20% for an equipped item. const uint32_t maxDur = it->second.maxDurability; if (maxDur > 0 && val < maxDur / 5u && prevDur >= maxDur / 5u) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 63a747ae..d29ef76e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5936,6 +5936,11 @@ void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) { if (inventoryHandler_) inventoryHandler_->repairAll(vendorGuid, useGuildBank); } +uint32_t GameHandler::estimateRepairAllCost() const { + if (inventoryHandler_) return inventoryHandler_->estimateRepairAllCost(); + return 0; +} + void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) { if (inventoryHandler_) inventoryHandler_->sellItem(vendorGuid, itemGuid, count); } diff --git a/src/game/inventory_handler.cpp b/src/game/inventory_handler.cpp index f61d2d4c..6d90d6d5 100644 --- a/src/game/inventory_handler.cpp +++ b/src/game/inventory_handler.cpp @@ -10,6 +10,8 @@ #include "core/logger.hpp" #include "network/world_socket.hpp" #include "network/packet.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" #include "pipeline/dbc_layout.hpp" #include #include @@ -697,9 +699,8 @@ void InventoryHandler::handleLootResponse(network::Packet& packet) { const bool wotlkLoot = isActiveExpansion("wotlk"); if (!LootResponseParser::parse(packet, currentLoot_, wotlkLoot)) return; const bool hasLoot = !currentLoot_.items.empty() || currentLoot_.gold > 0; - LOG_WARNING("[GO-DIAG] SMSG_LOOT_RESPONSE: guid=0x", std::hex, currentLoot_.lootGuid, std::dec, - " items=", currentLoot_.items.size(), " gold=", currentLoot_.gold, - " hasLoot=", hasLoot); + LOG_DEBUG("SMSG_LOOT_RESPONSE: guid=0x", std::hex, currentLoot_.lootGuid, std::dec, + " items=", currentLoot_.items.size(), " gold=", currentLoot_.gold); if (!hasLoot && owner_.isCasting() && owner_.getCurrentCastSpellId() != 0 && lastInteractedGoGuid_ != 0) { LOG_DEBUG("Ignoring empty SMSG_LOOT_RESPONSE during gather cast"); return; @@ -1029,22 +1030,58 @@ void InventoryHandler::buyBackItem(uint32_t buybackSlot) { void InventoryHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) { if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return; + + uint32_t cost = estimateItemRepairCost(itemGuid); + if (cost > 0 && owner_.getMoneyCopper() < cost) { + owner_.addUIError("Not enough money"); + return; + } + network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM)); packet.writeUInt64(vendorGuid); packet.writeUInt64(itemGuid); - packet.writeUInt8(0); + if (!isClassicLikeExpansion()) packet.writeUInt8(0); owner_.socket->send(packet); + + // Only do optimistic update if we verified the player can afford it + if (cost > 0) { + owner_.playerMoneyCopper_ -= cost; + auto it = owner_.onlineItems_.find(itemGuid); + if (it != owner_.onlineItems_.end()) { + it->second.curDurability = it->second.maxDurability; + rebuildOnlineInventory(); + } + } } void InventoryHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) { if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return; + + uint32_t totalCost = estimateRepairAllCost(); + + if (!useGuildBank && totalCost > 0 && owner_.getMoneyCopper() < totalCost) { + owner_.addUIError("Not enough money"); + return; + } + network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM)); packet.writeUInt64(vendorGuid); packet.writeUInt64(0); - packet.writeUInt8(useGuildBank ? 1 : 0); + if (!isClassicLikeExpansion()) packet.writeUInt8(useGuildBank ? 1 : 0); owner_.socket->send(packet); - LOG_INFO("Sent CMSG_REPAIR_ITEM repairAll vendor=0x", std::hex, vendorGuid, - std::dec, " guildBank=", useGuildBank ? 1 : 0); + + // Only do optimistic update if we verified the player can afford it + if (totalCost > 0) { + if (!useGuildBank) { + owner_.playerMoneyCopper_ -= totalCost; + } + for (auto& [guid, info] : owner_.onlineItems_) { + if (info.maxDurability > 0 && info.curDurability < info.maxDurability) { + info.curDurability = info.maxDurability; + } + } + rebuildOnlineInventory(); + } } void InventoryHandler::autoEquipItemBySlot(int backpackIndex) { @@ -1088,9 +1125,8 @@ void InventoryHandler::useItemBySlot(int backpackIndex) { break; } } - LOG_WARNING("useItemBySlot: item='", slot.item.name, "' entry=", slot.item.itemId, - " guid=0x", std::hex, itemGuid, std::dec, - " spellId=", useSpellId, " spellCount=", info->spells.size()); + LOG_DEBUG("useItemBySlot: entry=", slot.item.itemId, + " spellId=", useSpellId); } auto packet = owner_.packetParsers_ ? owner_.packetParsers_->buildUseItem(0xFF, static_cast(Inventory::NUM_EQUIP_SLOTS + backpackIndex), itemGuid, useSpellId) @@ -1300,7 +1336,6 @@ void InventoryHandler::handleListInventory(network::Packet& packet) { } } } - vendorWindowOpen_ = true; owner_.closeGossip(); if (owner_.addonEventCallback_) owner_.addonEventCallback_("MERCHANT_SHOW", {}); @@ -3255,5 +3290,94 @@ void InventoryHandler::addMoneyCopper(uint32_t amount) { owner_.fireAddonEvent("CHAT_MSG_MONEY", {msg}); } +// ============================================================ +// Repair cost estimation from DBC data +// ============================================================ + +void InventoryHandler::loadRepairDbc() const { + if (repairDbcLoaded_) return; + repairDbcLoaded_ = true; + + auto* am = owner_.services().assetManager; + if (!am || !am->isInitialized()) return; + + // DurabilityCosts.dbc: field 0 = itemLevel (key), fields 1-29 = cost multipliers + // Columns 1-21 = weapon subclass (0-20), columns 22-29 = armor subclass (0-7) + auto costsDbc = am->loadDBC("DurabilityCosts.dbc"); + if (costsDbc && costsDbc->isLoaded()) { + uint32_t count = costsDbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t itemLevel = costsDbc->getUInt32(i, 0); + std::array mults{}; + for (uint32_t f = 0; f < 29; ++f) + mults[f] = costsDbc->getUInt32(i, f + 1); + durabilityCosts_[itemLevel] = mults; + } + } + + // DurabilityQuality.dbc: field 0 = id (key), field 1 = quality_mod (float) + auto qualDbc = am->loadDBC("DurabilityQuality.dbc"); + if (qualDbc && qualDbc->isLoaded()) { + uint32_t count = qualDbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t id = qualDbc->getUInt32(i, 0); + float mod = qualDbc->getFloat(i, 1); + durabilityQuality_[id] = mod; + } + } +} + +uint32_t InventoryHandler::estimateItemRepairCost(uint64_t itemGuid) const { + auto itemIt = owner_.onlineItems_.find(itemGuid); + if (itemIt == owner_.onlineItems_.end()) return 0; + const auto& item = itemIt->second; + + if (item.maxDurability == 0 || item.curDurability >= item.maxDurability) return 0; + uint32_t lostDur = item.maxDurability - item.curDurability; + + auto infoIt = owner_.itemInfoCache_.find(item.entry); + if (infoIt == owner_.itemInfoCache_.end()) return 0; + const auto& info = infoIt->second; + + loadRepairDbc(); + + // Look up DurabilityCosts multiplier for this itemLevel + auto costIt = durabilityCosts_.find(info.itemLevel); + if (costIt == durabilityCosts_.end()) return 0; + + // Determine column index: weapon (class=2) uses subClass directly, + // armor (class=4) uses subClass + 21 + uint32_t colIndex = 0; + if (info.itemClass == 2) { // ITEM_CLASS_WEAPON + colIndex = info.subClass; + } else if (info.itemClass == 4) { // ITEM_CLASS_ARMOR + colIndex = info.subClass + 21; + } else { + return 0; // only weapons and armor have durability + } + if (colIndex >= 29) return 0; + + uint32_t dmultiplier = costIt->second[colIndex]; + if (dmultiplier == 0) return 0; + + // Quality modifier lookup: index is (quality + 1) * 2 + uint32_t qualIndex = (info.quality + 1) * 2; + auto qualIt = durabilityQuality_.find(qualIndex); + if (qualIt == durabilityQuality_.end()) return 0; + float qualMod = qualIt->second; + + uint32_t cost = static_cast(lostDur * dmultiplier * qualMod); + if (cost == 0 && lostDur > 0) cost = 1; // minimum 1 copper + return cost; +} + +uint32_t InventoryHandler::estimateRepairAllCost() const { + uint32_t total = 0; + for (const auto& [guid, info] : owner_.onlineItems_) { + total += estimateItemRepairCost(guid); + } + return total; +} + } // namespace game } // namespace wowee diff --git a/src/game/quest_handler.cpp b/src/game/quest_handler.cpp index b4a1aad6..4b32ae33 100644 --- a/src/game/quest_handler.cpp +++ b/src/game/quest_handler.cpp @@ -948,8 +948,7 @@ void QuestHandler::selectGossipOption(uint32_t optionId) { } auto pkt = ListInventoryPacket::build(currentGossip_.npcGuid); owner_.socket->send(pkt); - LOG_INFO("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip_.npcGuid, std::dec, - " vendor=", (int)isVendor, " repair=", (int)isArmorer); + LOG_DEBUG("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip_.npcGuid, std::dec); } if (textLower.find("make this inn your home") != std::string::npos || diff --git a/src/ui/window_manager.cpp b/src/ui/window_manager.cpp index 19e1c81f..0ddbedeb 100644 --- a/src/ui/window_manager.cpp +++ b/src/ui/window_manager.cpp @@ -896,11 +896,16 @@ void WindowManager::renderVendorWindow(game::GameHandler& gameHandler, renderCoinsFromCopper(money); if (vendor.canRepair) { + uint32_t repairCost = gameHandler.estimateRepairAllCost(); ImGui::SameLine(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f); if (ImGui::SmallButton("Repair All")) { gameHandler.repairAll(vendor.vendorGuid, false); } + if (repairCost > 0) { + ImGui::SameLine(0, 4); + renderCoinsFromCopper(repairCost); + } if (ImGui::IsItemHovered()) { // Show durability summary of all equipment const auto& inv = gameHandler.getInventory(); @@ -925,7 +930,7 @@ void WindowManager::renderVendorWindow(game::GameHandler& gameHandler, gameHandler.repairAll(vendor.vendorGuid, true); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Repair all equipped items using guild bank funds"); + ImGui::SetTooltip("Repair all items using guild bank funds"); } } }