Files
Bagshui/Components/ActiveQuestItems.lua
OEVGandClaude Opus 5.5 5f0c1c95d6 Re-categorize only changed items; per-frame work only on hovered button
- CategorizeItems() keeps each item's category unless something it was
  based on changed (item, bag/slot, bag type, count, charges, stock
  state, tooltip). A full pass runs on window open, forced resort,
  rebuilt lookup tables, character switch and any non-bag event.
- ActiveQuestItems: raise BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE only when the
  quest item list or counts changed, not after every QUEST_LOG_UPDATE.
- Item buttons: OnUpdate is set on mouse enter and removes itself after
  the mouse leaves; the base template OnUpdate is cleared. Stock badges
  are refreshed once a minute from the window's OnUpdate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
2026-11-01 20:57:18 +00:00

174 lines
7.6 KiB
Lua

-- Bagshui Active Quest Item Manager
-- Exposes: Bagshui.components.ActiveQuestItemManager
-- Raises: BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE
--
-- Contains code based on QuestItem: https://github.com/wow-vanilla-addons/QuestItem/blob/master/QuestItem/QuestItem.lua
--
-- Keeps Bagshui.activeQuestItems updated with the NAMES of items which are objectives of current quests.
-- (We can't get item IDs due to Vanilla limitations).
--
-- Raises BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE when changes to Bagshui.activeQuestItems occur.
--
-- This works by receiving QUEST_LOG_UPDATE events, which tend to come in fast and furious. To avoid constantly
-- rebuilding the list of active quest items, we're using an event queue:
-- 1. QUEST_LOG_UPDATE event comes in and BAGSHUI_QUEST_LOG_UPDATE event is queued to fire 1 second later.
-- If another QUEST_LOG_UPDATE event arrives before that 1 second is up, the timer is reset.
-- 2. BAGSHUI_QUEST_LOG_UPDATE event fires and triggers the actual parsing of the quest log.
-- After that is complete, BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE event is fired immediately.
-- 3. Other components that have registered to received BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE are now notified
-- and can trigger recategorization and resorting.
Bagshui:AddComponent(function()
-- Key in _G.BagshuiData where active quest items will be stored.
local ACTIVE_QUEST_ITEMS_DATA_STORAGE_KEY = "activeQuestItems"
-- Initialization (happens when this component is loaded during `ADDON_LOADED`)
if not Bagshui.currentCharacterData[ACTIVE_QUEST_ITEMS_DATA_STORAGE_KEY] then
Bagshui.currentCharacterData[ACTIVE_QUEST_ITEMS_DATA_STORAGE_KEY] = {}
end
local ActiveQuestItemManager = {
-- List of active quest items by item name.
--
-- Items must be stored in SavedVariables to to prevent inventory items from
-- jumping around during UI reload, because there is a delay before the quest
-- log can be read. The list is when QUEST_LOG_UPDATE fires for the first time.
-- ```
-- {
-- ["Item Name"] = {
-- questName = "Quest Name",
-- needed = #,
-- obtained = #,
-- }
-- }
--
-- ```
---@type table<string, { questName: string, needed: number, obtained: number }>
items = Bagshui.currentCharacterData[ACTIVE_QUEST_ITEMS_DATA_STORAGE_KEY],
}
--- Event handling.
---@param event string Event name.
---@param arg1 any Event argument.
function ActiveQuestItemManager:OnEvent(event, arg1)
-- WoW says "Something has happened with the quest log!" (it says this a lot).
-- Queue an event so that we only do our update after all the events have finished firing
-- This will be the most common event received, by far, so it goes first.
if event == "QUEST_LOG_UPDATE" then
Bagshui:QueueEvent("BAGSHUI_QUEST_LOG_UPDATE", 1)
return
end
-- Enough time has elapsed since the last QUEST_LOG_UPDATE event to actually do our update.
if event == "BAGSHUI_QUEST_LOG_UPDATE" then
-- Only tell inventories when quest items actually changed. QUEST_LOG_UPDATE fires very often,
-- and every BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE makes each open inventory re-categorize everything.
if self:Update() then
Bagshui:RaiseEvent("BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE")
end
return
end
end
--- Parse the quest log to find the names of items that are quest objectives.
---@return boolean changed Whether the list of quest items (or their counts) changed.
function ActiveQuestItemManager:Update()
-- The quest log is one of those annoying things where you can't directly ask
-- about a quest; you have the tell the client to *select* the quest, and then
-- all the quest info functions work on that. This becomes a problem if the
-- Quest Log is open and we want to update, since we need to loop through
-- all quests, we're going to have to change the selected quest. What ends up
-- happening is that once we're done, the very last quest in the log remains
-- selected and is out of sync with the Quest Log UI. This matters when someone
-- abandons multiple quests in a row, because the first abandonment will be
-- accurate, but then the next Abandon Quest click will target the last quest
-- in the list instead of the one that looks like it's selected.
-- All that to say -- we need to store the currently selected quest number
-- before we start so it can be restored once we're done.
local previouslySelectedQuestNum = _G.GetQuestLogSelection()
-- Keep the previous contents to find out whether anything changed. (`self.items` itself must
-- stay the same table because it's exported as `Bagshui.activeQuestItems`.)
if not self.previousItems then
self.previousItems = {}
end
BsUtil.TableClear(self.previousItems)
for name, info in pairs(self.items) do
self.previousItems[name] = info
end
-- In theory we could keep a list of the items we've actually seen and remove the ones
-- we don't see, but erasing it is easier. (Can always change if it's not performant enough).
BsUtil.TableClear(self.items)
local questName, isHeader, objectiveText, itemType, itemName, numNeeded, numObtained
for questNum = 1, _G.GetNumQuestLogEntries(), 1 do
-- Only need a couple pieces of information from GetQuestLogTitle.
questName, _, _, isHeader, _, _ = _G.GetQuestLogTitle(questNum)
if not isHeader then
itemName = nil
numNeeded = 1
numObtained = 0
-- All subsequent function calls depend on selecting the quest log entry.
_G.SelectQuestLogEntry(questNum)
-- If description and quest objective text is ever needed, here's how to get it:
-- local questionDescription, questObjectiveText = _G.GetQuestLogQuestText()
-- Quest objectives are in the "Leader Boards".
for i = 1, _G.GetNumQuestLeaderBoards() do
objectiveText, itemType = _G.GetQuestLogLeaderBoard(i)
-- We only care if type is item/object (not monster, event, etc.).
if itemType ~= nil and (itemType == "item" or itemType == "object") then
-- Parse and store the item information from the objective text, which is formatted as:
-- `<Item Name>: <Number Obtained>/<Total Needed>`
_, _, itemName, numObtained, numNeeded = string.find(objectiveText, "(.+): (%d+)/(%d+)")
if itemName then
self.items[itemName] = {
questName = questName,
needed = numNeeded,
obtained = numObtained,
}
end
end
end
end
end
-- Restore Quest Log selection, if any (see the dissertation at the beginning of this function).
if previouslySelectedQuestNum then
_G.SelectQuestLogEntry(previouslySelectedQuestNum)
end
-- Did anything change?
for name, info in pairs(self.items) do
local previous = self.previousItems[name]
if
not previous
or previous.questName ~= info.questName
or previous.needed ~= info.needed
or previous.obtained ~= info.obtained
then
return true
end
end
for name, _ in pairs(self.previousItems) do
if not self.items[name] then
return true
end
end
return false
end
-- Exports and registration.
Bagshui.components.ActiveQuestItemManager = ActiveQuestItemManager
Bagshui.activeQuestItems = ActiveQuestItemManager.items
Bagshui:RegisterEvent("QUEST_LOG_UPDATE", ActiveQuestItemManager) -- Quest log changes.
Bagshui:RegisterEvent("BAGSHUI_QUEST_LOG_UPDATE", ActiveQuestItemManager) -- Our custom trigger to parse the quest log.
end)