feat: implement item durability tracking and vendor repair

- Add ITEM_FIELD_DURABILITY (60) and ITEM_FIELD_MAXDURABILITY (61) to
  update_field_table.hpp enum and wotlk/update_fields.json
- Add curDurability/maxDurability to OnlineItemInfo and ItemDef structs
- Parse durability fields in OBJECT_CREATE and OBJECT_VALUES handlers;
  preserve existing values on partial updates (fixes stale durability
  being reset to 0 on stack-count-only updates)
- Propagate durability to ItemDef in all 5 rebuildOnlineInventory() paths
- Implement GameHandler::repairItem() and repairAll() via CMSG_REPAIR_ITEM
  (itemGuid=0 repairs all equipped items per WotLK protocol)
- Add canRepair flag to ListInventoryData; set it when player selects
  GOSSIP_OPTION_ARMORER in gossip window
- Show "Repair All" button in vendor window header when canRepair=true
- Display color-coded durability in item tooltip (green >50%, yellow
  >25%, red <=25%)
This commit is contained in:
Kelsi 2026-03-10 16:21:09 -07:00
parent 094ef88e57
commit 0afa41e908
9 changed files with 96 additions and 10 deletions

View file

@ -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,