From 474da1a5b5b915ed888eeebb2f9c743aeb0af1d7 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:12:22 -0500 Subject: [PATCH] feat(lua): Item follows retail Util/Item.lua is now retail Blizzard_ObjectAPI/Mainline/Item.lua in retail's own text, and its listener is retail's AsyncCallbackSystem.lua with the item and quest listeners. There is no spell listener: every spell is client-side data here, so there is nothing to wait for. The one other difference is that GetItemQualityColor reads ITEM_QUALITY_COLORS, the same entry retail's ColorManager returns. What follows retail now: - Load callbacks get (id, success). - An item callback also runs when the load fails, with success nil. TradeSkillLink's two callbacks now stop on a failure, because re-rendering would request the missing item again. - C_Item.RequestLoadItemDataByID runs once per item, not once per callback. - New methods: GetStackCount, ContinueWithCancelOnRecordLoad and IsRecordDataCached. ContinueOnItemLoad's usage errors name the bad itemID or link, and GetItemGUID returns the GUID an item was made from. C_Item.GetItemIDForItemInfo is new in the DLL for ItemMixin:GetItemID's fallback. It uses the resolver that GetItemInfoInstant uses. Verified in game: Item:CreateFromItemID(19019) calls back with (19019, 1), and C_Item.GetItemIDForItemInfo("item:6948") returns 6948. --- TODO.md | 2 +- .../AddOns/!!!ClassicAPI/!!!ClassicAPI.toc | 1 + .../Util/AsyncCallbackSystem.lua | 116 +++++ src/embed/AddOns/!!!ClassicAPI/Util/Item.lua | 417 ++++++++---------- .../!!!ClassicAPI/Util/TradeSkillLink.lua | 14 +- src/item/Info.cpp | 24 + 6 files changed, 330 insertions(+), 244 deletions(-) create mode 100644 src/embed/AddOns/!!!ClassicAPI/Util/AsyncCallbackSystem.lua diff --git a/TODO.md b/TODO.md index 2f55bcd1..040e2056 100644 --- a/TODO.md +++ b/TODO.md @@ -149,7 +149,7 @@ on a frame timer" goal landed as the request/event pair below: cache (or synchronously if it was already loaded). Together those replace the polyfill's 180-second polling frame -(`ItemEventListener` in `Util/ItemUtil.lua:254-372`) — addons can now +(`ItemEventListener`, now in `Util/AsyncCallbackSystem.lua`) — addons can now fire `RequestLoadItemDataByID(id)` and listen for `ITEM_DATA_LOAD_RESULT` instead. diff --git a/src/embed/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc b/src/embed/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc index e00ddbf8..aa340131 100644 --- a/src/embed/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc +++ b/src/embed/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc @@ -41,6 +41,7 @@ Util\ColorUtil.lua Util\ItemLocation.lua Util\PlayerLocation.lua Util\EventUtil.lua +Util\AsyncCallbackSystem.lua Util\Item.lua Util\AddOnCompat.lua Util\FunctionUtil.lua diff --git a/src/embed/AddOns/!!!ClassicAPI/Util/AsyncCallbackSystem.lua b/src/embed/AddOns/!!!ClassicAPI/Util/AsyncCallbackSystem.lua new file mode 100644 index 00000000..82c25d0b --- /dev/null +++ b/src/embed/AddOns/!!!ClassicAPI/Util/AsyncCallbackSystem.lua @@ -0,0 +1,116 @@ +-- Backport of retail's Blizzard_ObjectAPI/Mainline/AsyncCallbackSystem.lua. +-- +-- Differences from the retail source: +-- - No ASYNC_SPELL type and no SpellEventListener. This client keeps every +-- spell in its own data files, so there is no spell data to wait for and +-- no C_Spell.RequestLoadSpellData / SPELL_DATA_LOAD_RESULT. + +--[[ + Queries some data retrieval API (specifically where the data may not be currently available) and when it becomes available + calls a user-supplied function. The callback can be canceled if necessary (e.g. the frame that would use the data becomes + hidden before the data arrives). + + The API is managed so that arbitrary query functions cannot be executed. +--]] + +AsyncCallbackAPIType = { + ASYNC_QUEST = 1, + ASYNC_ITEM = 2, +} + +local permittedAPI = +{ + [AsyncCallbackAPIType.ASYNC_QUEST] = { event = "QUEST_DATA_LOAD_RESULT", accessor = C_QuestLog.RequestLoadQuestByID }, + [AsyncCallbackAPIType.ASYNC_ITEM] = { event = "ITEM_DATA_LOAD_RESULT", accessor = C_Item.RequestLoadItemDataByID }, +}; + +AsyncCallbackSystemMixin = {}; + +function AsyncCallbackSystemMixin:Init(apiType) + self.callbacks = {}; + + -- API Type should be set up from key value pairs before OnLoad. + self.api = permittedAPI[apiType]; + + self:SetScript("OnEvent", + function(self, event, ...) + if event == self.api.event then + local id, success = ...; + if success or apiType == AsyncCallbackAPIType.ASYNC_ITEM then + self:FireCallbacks(id, success); + else + self:ClearCallbacks(id); + end + end + end + ); + self:RegisterEvent(self.api.event); +end + +local CANCELED_SENTINEL = -1; + +function AsyncCallbackSystemMixin:AddCallback(id, callbackFunction) + local callbacks = self:GetOrCreateCallbacks(id); + table.insert(callbacks, callbackFunction); + local needsAccessorCall = #callbacks == 1; + if needsAccessorCall then + self.api.accessor(id); + end + + return #callbacks, callbacks; +end + +function AsyncCallbackSystemMixin:AddCancelableCallback(id, callbackFunction) + -- NOTE: If the data is currently availble then the callback will be executed and callbacks cleared, so there will be nothing to cancel. + local index, callbacks = self:AddCallback(id, callbackFunction); + return function() + if #callbacks > 0 and callbacks[index] ~= CANCELED_SENTINEL then + callbacks[index] = CANCELED_SENTINEL; + return true; + end + return false; + end; +end + +function AsyncCallbackSystemMixin:FireCallbacks(id, success) + local callbacks = self:GetCallbacks(id); + if callbacks then + self:ClearCallbacks(id); + for i, callback in ipairs(callbacks) do + if callback ~= CANCELED_SENTINEL then + xpcall(callback, CallErrorHandler, id, success); + end + end + + -- The cancel functions have a reference to this table, so ensure that it's cleared out. + for i = #callbacks, 1, -1 do + callbacks[i] = nil; + end + end +end + +function AsyncCallbackSystemMixin:ClearCallbacks(id) + self.callbacks[id] = nil; +end + +function AsyncCallbackSystemMixin:GetCallbacks(id) + return self.callbacks[id]; +end + +function AsyncCallbackSystemMixin:GetOrCreateCallbacks(id) + local callbacks = self.callbacks[id]; + if not callbacks then + callbacks = {}; + self.callbacks[id] = callbacks; + end + return callbacks; +end + +local function CreateListener(apiType) + local listener = Mixin(CreateFrame("Frame"), AsyncCallbackSystemMixin); + listener:Init(apiType); + return listener; +end + +ItemEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_ITEM); +QuestEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_QUEST); diff --git a/src/embed/AddOns/!!!ClassicAPI/Util/Item.lua b/src/embed/AddOns/!!!ClassicAPI/Util/Item.lua index 12a61409..fcf27bac 100644 --- a/src/embed/AddOns/!!!ClassicAPI/Util/Item.lua +++ b/src/embed/AddOns/!!!ClassicAPI/Util/Item.lua @@ -1,70 +1,55 @@ --- Backport of 3.3.5 FrameXML/ItemUtil.lua to vanilla 1.12 / Lua 5.0. +-- Backport of retail's Blizzard_ObjectAPI/Mainline/Item.lua. -- --- Implementation differences from the 3.3.5 source: --- - Many C_Item.* calls the modern code expects are not exposed by --- ClassicAPI.dll. We shim the missing ones at the top using the --- stock 1.12 globals (GetItemInfo, GetInventoryItemLink, --- GetContainerItemLink/Info). DLL-provided functions are left alone --- via `or` guards so we never clobber a real implementation. --- - Lua 5.0 has no select(); calls that previously did --- `select(N, GetItemInfo(...))` use named locals to peel off the --- return values instead. --- - CallErrorHandler isn't in 1.12; we route through geterrorhandler(). --- - In 1.12 OnEvent scripts, event name + payload are exposed via the --- `event`, `arg1`, `arg2`... globals rather than as function args. --- - 1.12 has no concept of an item GUID and no public item-lock API, --- so those return nil / no-op. +-- Differences from the retail source: +-- - GetItemQualityColor reads ITEM_QUALITY_COLORS directly; retail's +-- ColorManager.GetColorDataForItemQuality returns the same entry. ----------------------------------------------------------------------- - -Item = {} -ItemMixin = {} - -local ItemEventListener +Item = {}; +ItemMixin = {}; --[[static]] function Item:CreateFromItemLocation(itemLocation) - if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then - error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2) - end - local item = CreateFromMixins(ItemMixin) - item:SetItemLocation(itemLocation) - return item + if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then + error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2); + end + local item = CreateFromMixins(ItemMixin); + item:SetItemLocation(itemLocation); + return item; end --[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex) - if type(bagID) ~= "number" or type(slotIndex) ~= "number" then - error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2) - end - local item = CreateFromMixins(ItemMixin) - item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex)) - return item + if type(bagID) ~= "number" or type(slotIndex) ~= "number" then + error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2); + end + local item = CreateFromMixins(ItemMixin); + item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex)); + return item; end --[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex) - if type(equipmentSlotIndex) ~= "number" then - error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2) - end - local item = CreateFromMixins(ItemMixin) - item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex)) - return item + if type(equipmentSlotIndex) ~= "number" then + error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2); + end + local item = CreateFromMixins(ItemMixin); + item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex)); + return item; end --[[static]] function Item:CreateFromItemLink(itemLink) - if type(itemLink) ~= "string" then - error("Usage: Item:CreateFromItemLink(itemLinkString)", 2) - end - local item = CreateFromMixins(ItemMixin) - item:SetItemLink(itemLink) - return item + if type(itemLink) ~= "string" then + error("Usage: Item:CreateFromItemLink(itemLinkString)", 2); + end + local item = CreateFromMixins(ItemMixin); + item:SetItemLink(itemLink); + return item; end --[[static]] function Item:CreateFromItemID(itemID) - if type(itemID) ~= "number" then - error("Usage: Item:CreateFromItemID(itemID)", 2) - end - local item = CreateFromMixins(ItemMixin) - item:SetItemID(itemID) - return item + if type(itemID) ~= "number" then + error("Usage: Item:CreateFromItemID(itemID)", 2); + end + local item = CreateFromMixins(ItemMixin); + item:SetItemID(itemID); + return item; end --[[static]] function Item:CreateFromItemGUID(itemGUID) @@ -103,18 +88,18 @@ function ItemMixin:Matches(item) end function ItemMixin:SetItemLocation(itemLocation) - self:Clear() - self.itemLocation = itemLocation + self:Clear(); + self.itemLocation = itemLocation; end function ItemMixin:SetItemLink(itemLink) - self:Clear() - self.itemLink = itemLink + self:Clear(); + self.itemLink = itemLink; end function ItemMixin:SetItemID(itemID) - self:Clear() - self.itemID = itemID + self:Clear(); + self.itemID = itemID; end function ItemMixin:SetItemGUID(itemGUID) @@ -133,129 +118,149 @@ function ItemMixin:GetItemLocation() return nil; end +function ItemMixin:GetItemGUID() + if self.itemGUID then + return self.itemGUID; + end + + if self.itemLocation then + return C_Item.GetItemGUID(self.itemLocation); + end + + return nil; +end + function ItemMixin:HasItemLocation() - return self.itemLocation ~= nil + return self.itemLocation ~= nil; end function ItemMixin:Clear() - self.itemLocation = nil - self.itemLink = nil - self.itemID = nil - self.itemGUID = nil + self.itemLocation = nil; + self.itemLink = nil; + self.itemID = nil; + self.itemGUID = nil; end function ItemMixin:IsItemEmpty() - if self:GetStaticBackingItem() then - return not C_Item.DoesItemExistByID(self:GetStaticBackingItem()) - end + if self:GetStaticBackingItem() then + return not C_Item.DoesItemExistByID(self:GetStaticBackingItem()); + end - return not self:IsItemInPlayersControl() + return not self:IsItemInPlayersControl(); end function ItemMixin:GetStaticBackingItem() - return self.itemLink or self.itemID + return self.itemLink or self.itemID; end function ItemMixin:IsItemInPlayersControl() - local itemLocation = self:GetItemLocation() - return itemLocation and C_Item.DoesItemExist(itemLocation) + local itemLocation = self:GetItemLocation(); + return itemLocation and C_Item.DoesItemExist(itemLocation); end -- Item API function ItemMixin:GetItemID() - if self:GetStaticBackingItem() then - return (C_Item.GetItemInfoInstant(self:GetStaticBackingItem())) - end + local backingItem = self:GetStaticBackingItem(); + if backingItem then + return C_Item.GetItemInfoInstant(backingItem) or C_Item.GetItemIDForItemInfo(backingItem); + end - if not self:IsItemEmpty() then - return C_Item.GetItemID(self:GetItemLocation()) - end - return nil + if not self:IsItemEmpty() then + return C_Item.GetItemID(self:GetItemLocation()); + end + + return nil; end function ItemMixin:IsItemLocked() - return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation()) + return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation()); end function ItemMixin:LockItem() - if self:IsItemInPlayersControl() then - C_Item.LockItem(self:GetItemLocation()) - end + if self:IsItemInPlayersControl() then + C_Item.LockItem(self:GetItemLocation()); + end end function ItemMixin:UnlockItem() - if self:IsItemInPlayersControl() then - C_Item.UnlockItem(self:GetItemLocation()) - end + if self:IsItemInPlayersControl() then + C_Item.UnlockItem(self:GetItemLocation()); + end end function ItemMixin:GetItemIcon() -- requires item data to be loaded - if self:GetStaticBackingItem() then - return C_Item.GetItemIconByID(self:GetStaticBackingItem()) - end + if self:GetStaticBackingItem() then + return C_Item.GetItemIconByID(self:GetStaticBackingItem()); + end - if not self:IsItemEmpty() then - return C_Item.GetItemIcon(self:GetItemLocation()) - end + if not self:IsItemEmpty() then + return C_Item.GetItemIcon(self:GetItemLocation()); + end end function ItemMixin:GetItemName() -- requires item data to be loaded - if self:GetStaticBackingItem() then - return C_Item.GetItemNameByID(self:GetStaticBackingItem()) - end + if self:GetStaticBackingItem() then + return C_Item.GetItemNameByID(self:GetStaticBackingItem()); + end - if not self:IsItemEmpty() then - return C_Item.GetItemName(self:GetItemLocation()) - end - return nil + if not self:IsItemEmpty() then + return C_Item.GetItemName(self:GetItemLocation()); + end + return nil; end function ItemMixin:GetItemLink() -- requires item data to be loaded - if self.itemLink then - return self.itemLink - end + if self.itemLink then + return self.itemLink; + end - if self.itemID then - return (select(2, C_Item.GetItemInfo(self.itemID))); - end + if self.itemID then + return (select(2, C_Item.GetItemInfo(self.itemID))); + end - if not self:IsItemEmpty() then - return C_Item.GetItemLink(self:GetItemLocation()) - end - return nil + if not self:IsItemEmpty() then + return C_Item.GetItemLink(self:GetItemLocation()); + end + return nil; end function ItemMixin:GetItemQuality() -- requires item data to be loaded - if self:GetStaticBackingItem() then - return C_Item.GetItemQualityByID(self:GetStaticBackingItem()) - end + if self:GetStaticBackingItem() then + return C_Item.GetItemQualityByID(self:GetStaticBackingItem()); + end - if not self:IsItemEmpty() then - return C_Item.GetItemQuality(self:GetItemLocation()) - end - return nil + if not self:IsItemEmpty() then + return C_Item.GetItemQuality(self:GetItemLocation()); + end + return nil; +end + +function ItemMixin:GetStackCount() -- requires item data to be loaded + if not self:IsItemEmpty() then + return C_Item.GetStackCount(self:GetItemLocation()); + end + return nil; end function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded - if self:GetStaticBackingItem() then - return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem())) - end + if self:GetStaticBackingItem() then + return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem())); + end - if not self:IsItemEmpty() then - return C_Item.GetCurrentItemLevel(self:GetItemLocation()) - end - return nil + if not self:IsItemEmpty() then + return C_Item.GetCurrentItemLevel(self:GetItemLocation()); + end + return nil; end function ItemMixin:GetItemQualityColor() -- requires item data to be loaded - local itemQuality = self:GetItemQuality() - return ITEM_QUALITY_COLORS[itemQuality] -- may be nil if item data isn't loaded + local itemQuality = self:GetItemQuality(); + return ITEM_QUALITY_COLORS[itemQuality]; -- may be nil if item data isn't loaded end - function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded - local colorTbl = self:GetItemQualityColor() - return colorTbl.color:GetRGB() + local colorTbl = self:GetItemQualityColor(); + return colorTbl.color:GetRGB(); end function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded @@ -275,141 +280,75 @@ function ItemMixin:IsStackable() -- requires item data to be loaded end function ItemMixin:GetInventoryType() - if self:GetStaticBackingItem() then - return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem()) - end + if self:GetStaticBackingItem() then + return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem()); + end - if not self:IsItemEmpty() then - return C_Item.GetItemInventoryType(self:GetItemLocation()) - end - return nil -end - -function ItemMixin:GetItemGUID() - if self:GetStaticBackingItem() then - return nil - end - - if not self:IsItemEmpty() then - return C_Item.GetItemGUID(self:GetItemLocation()) - end - return nil + if not self:IsItemEmpty() then + return C_Item.GetItemInventoryType(self:GetItemLocation()); + end + return nil; end function ItemMixin:GetInventoryTypeName() - if not self:IsItemEmpty() then - return select(4, C_Item.GetItemInfoInstant(self:GetItemID())); - end + if not self:IsItemEmpty() then + return select(4, C_Item.GetItemInfoInstant(self:GetItemID())); + end end function ItemMixin:IsItemDataCached() - if self:GetStaticBackingItem() then - return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem()) - end + if self:GetStaticBackingItem() then + return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem()); + end - if not self:IsItemEmpty() then - return C_Item.IsItemDataCached(self:GetItemLocation()) - end - return true + if not self:IsItemEmpty() then + return C_Item.IsItemDataCached(self:GetItemLocation()); + end + return true; end function ItemMixin:IsDataEvictable() - return true + -- Item data could be evicted from the cache + return true; end --- Default error handler routing: stock 1.12 has no CallErrorHandler. -local function ItemUtilErrorHandler(err) - local h = geterrorhandler and geterrorhandler() - if h then return h(err) end +function ItemMixin:ValidateForContinueOnItemLoad(methodName, callbackFunction) + if type(callbackFunction) ~= "function" then + error(("Usage: NonEmptyItem:%s(callbackFunction): invalid callbackFunction"):format(methodName), 3); + end + + if self:IsItemEmpty() then + if self.itemLink then + error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemLink: <%s>"):format(methodName, self.itemLink), 3); + elseif self.itemID then + error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemID: <%d>"):format(methodName, self.itemID), 3); + end + + error(("Usage: NonEmptyItem:%s(callbackFunction): invalid item"):format(methodName), 3); + else + if not self:GetItemID() then + error(("Usage: NonEmptyItem:%s(callbackFunction): item appeared non-empty, but had invalid itemID. itemID: <%s>, itemLink: <%s>"):format(methodName, tostring(self.itemID), tostring(self.itemLink)), 3); + end + end end +-- Add a callback to be executed when item data is loaded, if the item data is already loaded then execute it immediately function ItemMixin:ContinueOnItemLoad(callbackFunction) - if type(callbackFunction) ~= "function" or self:IsItemEmpty() then - error("Usage: NonEmptyItem:ContinueOnLoad(callbackFunction)", 2) - end - - ItemEventListener:AddCallback(self:GetItemID(), callbackFunction) + self:ValidateForContinueOnItemLoad("ContinueOnItemLoad", callbackFunction); + ItemEventListener:AddCallback(self:GetItemID(), callbackFunction); end +-- Same as ContinueOnItemLoad, except it returns a function that when called will cancel the continue function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction) - if type(callbackFunction) ~= "function" or self:IsItemEmpty() then - error("Usage: NonEmptyItem:ContinueWithCancelOnItemLoad(callbackFunction)", 2) - end - - return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction) + self:ValidateForContinueOnItemLoad("ContinueWithCancelOnItemLoad", callbackFunction); + return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction); end ---[ Item Event Listener ] - -ItemEventListener = CreateFrame("Frame") -ItemEventListener.callbacks = {} - --- 1.12 OnEvent receives no function args; event name and payload live in --- the `event`, `arg1`, ... globals. -ItemEventListener:SetScript("OnEvent", function() - if event == "ITEM_DATA_LOAD_RESULT" then - local itemID, success = arg1, arg2 - if success then - ItemEventListener:FireCallbacks(itemID) - else - ItemEventListener:ClearCallbacks(itemID) - end - end -end) -ItemEventListener:RegisterEvent("ITEM_DATA_LOAD_RESULT") - -local CANCELED_SENTINEL = -1 - -function ItemEventListener:AddCallback(itemID, callbackFunction) - local callbacks = self:GetOrCreateCallbacks(itemID) - table.insert(callbacks, callbackFunction) - C_Item.RequestLoadItemDataByID(itemID) +-- Generic aliases for use with ContinuableContainer +function ItemMixin:ContinueWithCancelOnRecordLoad(callbackFunction) + return self:ContinueWithCancelOnItemLoad(callbackFunction); end -function ItemEventListener:AddCancelableCallback(itemID, callbackFunction) - local callbacks = self:GetOrCreateCallbacks(itemID) - table.insert(callbacks, callbackFunction) - C_Item.RequestLoadItemDataByID(itemID) - - local index = table.getn(callbacks) - return function() - if table.getn(callbacks) > 0 and callbacks[index] ~= CANCELED_SENTINEL then - callbacks[index] = CANCELED_SENTINEL - return true - end - return false - end -end - -function ItemEventListener:FireCallbacks(itemID) - local callbacks = self:GetCallbacks(itemID) - if callbacks then - self:ClearCallbacks(itemID) - for i, callback in ipairs(callbacks) do - if callback ~= CANCELED_SENTINEL then - xpcall(callback, ItemUtilErrorHandler) - end - end - - for i = table.getn(callbacks), 1, -1 do - callbacks[i] = nil - end - end -end - -function ItemEventListener:ClearCallbacks(itemID) - self.callbacks[itemID] = nil -end - -function ItemEventListener:GetCallbacks(itemID) - return self.callbacks[itemID] -end - -function ItemEventListener:GetOrCreateCallbacks(itemID) - local callbacks = self.callbacks[itemID] - if not callbacks then - callbacks = {} - self.callbacks[itemID] = callbacks - end - return callbacks +function ItemMixin:IsRecordDataCached() + return self:IsItemDataCached(); end diff --git a/src/embed/AddOns/!!!ClassicAPI/Util/TradeSkillLink.lua b/src/embed/AddOns/!!!ClassicAPI/Util/TradeSkillLink.lua index 4eef2c4c..872930ac 100644 --- a/src/embed/AddOns/!!!ClassicAPI/Util/TradeSkillLink.lua +++ b/src/embed/AddOns/!!!ClassicAPI/Util/TradeSkillLink.lua @@ -182,8 +182,14 @@ local function ResolveSubclasses(list, onLoaded) if subType and subType ~= "" then r.subclass = subType; elseif onLoaded then - -- Not cached — re-derive when the item loads. - Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded); + -- Not cached — re-derive when the item loads. The callback + -- also runs when the load fails; re-deriving then would ask + -- for the item again. + Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(function(_, success) + if success then + onLoaded(); + end + end); end end if r.subclass and not present[r.subclass] then @@ -526,8 +532,8 @@ local function CreateFrame_TradeSkillLink() local function itemInfo(id) local item = Item:CreateFromItemID(id); if not item:IsItemDataCached() then - item:ContinueOnItemLoad(function() - if self:IsShown() and self.selectedSpellID == entry.spellID then + item:ContinueOnItemLoad(function(_, success) + if success and self:IsShown() and self.selectedSpellID == entry.spellID then self:RenderDetail(entry); end end); diff --git a/src/item/Info.cpp b/src/item/Info.cpp index 5725a85f..161265ba 100644 --- a/src/item/Info.cpp +++ b/src/item/Info.cpp @@ -120,6 +120,28 @@ static int __fastcall Script_GetItemInfoInstant(void *L) { return 7; } +// `C_Item.GetItemIDForItemInfo(itemInfo)` — the itemID alone, from the same +// resolver `GetItemInfoInstant` echoes: an itemID, an `"item:N"` string or an +// item link. ItemMixin:GetItemID falls back to it when GetItemInfoInstant +// returns nothing. Returns nothing for input that names no item. +static const Game::Doc::Field kGetItemIDForItemInfoArgs[] = { + Game::Doc::Req("itemInfo", "ItemInfo"), +}; +static const Game::Doc::Field kGetItemIDForItemInfoRets[] = { + Game::Doc::Req("itemID", "number"), +}; +static const Game::Doc::Function kGetItemIDForItemInfo{ + "Returns the item ID of an item ID, item string or item link.", kGetItemIDForItemInfoArgs, + kGetItemIDForItemInfoRets}; + +static int __fastcall Script_GetItemIDForItemInfo(void *L) { + const int itemID = Item::Arg::ResolveItemID(L, 1); + if (itemID <= 0) + return 0; + Game::Lua::PushNumber(L, static_cast(itemID)); + return 1; +} + // Pushes the icon path for `itemID` and returns 1, or returns 0 (= nil // to Lua) if the item isn't cached, the displayInfoID is missing, or // the icon record's path slot is empty. Shared by all three @@ -315,6 +337,8 @@ static int __fastcall Script_C_Item_GetItemInfo(void *L) { static void RegisterLuaFunctions() { Game::Lua::RegisterTableFunction("C_Item", "GetItemInfo", &Script_C_Item_GetItemInfo); Game::Lua::RegisterTableFunction("C_Item", "GetItemInfoInstant", &Script_GetItemInfoInstant); + Game::Lua::RegisterTableFunction("C_Item", "GetItemIDForItemInfo", + &Script_GetItemIDForItemInfo, &kGetItemIDForItemInfo); Game::Lua::RegisterGlobalFunction("GetItemIcon", &Script_GetItemIcon); Game::Lua::RegisterTableFunction("C_Item", "GetItemIcon", &Script_C_Item_GetItemIcon); Game::Lua::RegisterTableFunction("C_Item", "GetItemIconByID", &Script_C_Item_GetItemIconByID);