enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
937 lines
40 KiB
Lua
937 lines
40 KiB
Lua
-- Bagshui Item Information
|
|
--
|
|
-- Obtain information about a given item.
|
|
-- Provide PeriodicTable interface.
|
|
-- Display a window with an edit box to provide copyable item information.
|
|
|
|
-- Using `LoadComponent()` to get `BsItemInfo` into the environment immediately so
|
|
-- `Bagshui:AddonLoaded()` can access `InitializeItem()` for inventory cache validation.
|
|
Bagshui:LoadComponent(function()
|
|
Bagshui:AddConstants({
|
|
|
|
---@enum BS_ITEM_INFO_DISPLAY_TYPE
|
|
-- Some item properties need to display differently depending on where they're
|
|
-- being shown. For example, periodicTable only provides a count of sets in
|
|
-- menus and tooltips, but gives the entire list of sets for text display.
|
|
-- This is a parameter for ItemInfo:ItemPropertyValuesForDisplay() and is passed
|
|
-- to the dynamic item property functions.
|
|
-- ```
|
|
-- { UPPERCASE = "UPPERCASE" }
|
|
-- ```
|
|
---@type table<string, string>
|
|
BS_ITEM_INFO_DISPLAY_TYPE = {
|
|
MENU = "MENU",
|
|
TEXT = "TEXT",
|
|
TOOLTIP = "TOOLTIP",
|
|
},
|
|
|
|
-- ItemInfo:InitializeItem() will never reset these properties.
|
|
---@type table<string, boolean>
|
|
BS_ITEM_INFO_PROTECTED_PROPERTIES = {
|
|
bagNum = true,
|
|
bagType = true,
|
|
slotNum = true,
|
|
},
|
|
|
|
-- Values for `itemUsableStatusCache`.
|
|
-- It's assumed that anything already known is also usable.
|
|
---@type table<string, string>
|
|
BS_ITEM_USABLE = {
|
|
NO = "No",
|
|
YES = "Yes",
|
|
KNOWN = "AlreadyKnown",
|
|
},
|
|
})
|
|
|
|
local ItemInfo = {
|
|
-- Last set of periodic table sets, which correspond to `periodicTableSetCacheItemLink`.
|
|
periodicTableSetCache = {},
|
|
|
|
-- This is the item for which the `periodicTableSetCache` was generated.
|
|
periodicTableSetCacheItemLink = "_", -- Not using "" so that empty slots don't match.
|
|
|
|
-- Usability of items, cached so we don't have to constantly reload tooltips.
|
|
itemUsableStatusCache = {},
|
|
-- Last update for each item in `itemUsableStatusCache`.
|
|
itemUsableStatusCacheTimestamps = {},
|
|
-- Cache invalidation.
|
|
itemUsableStatusMinTimestamp = 0,
|
|
|
|
-- Usability of items, cached so we don't have to constantly reload tooltips.
|
|
itemUsableStatusCache = {},
|
|
-- Last update for each item in `itemUsableStatusCache`.
|
|
itemUsableStatusCacheTimestamps = {},
|
|
-- Cache invalidation.
|
|
itemUsableStatusMinTimestamp = 0,
|
|
|
|
-- This is the ItemInfo window, declared in `ItemInfo:Init()`.
|
|
window = nil,
|
|
}
|
|
Bagshui.components.ItemInfo = ItemInfo
|
|
Bagshui.environment.BsItemInfo = ItemInfo
|
|
|
|
--- Given an item link or string, obtain as much information as possible.
|
|
--- # Important best practice for this function!
|
|
--- Except in special cases, provide fallback values from `BS_ITEM_SKELETON`:
|
|
--- ```
|
|
--- item.name = newName or BS_ITEM_SKELETON.name
|
|
--- ```
|
|
--- This makes it safe for other code to assume that item property values won't be nil.
|
|
--- Notable special cases, both used for empty slot differentiation:
|
|
--- * `itemString`
|
|
--- * `itemLink`
|
|
---@param itemIdentifier string|number|nil Item ID, link, or string. Allowed to be nil for empty slots.
|
|
---@param itemInfoTable table Table that will be filled with item information.
|
|
---@param initialize boolean? When true, always wipe the provided `itemInfoTable` before doing anything else.
|
|
---@param reinitialize boolean? `itemInfoTable` will be wiped after a successful call to `GetItemInfo()` when this is true.
|
|
---@param forceIntoLocalGameCache boolean? When true, call `LoadItemIntoLocalGameCache()` before doing anything else.
|
|
---@param inventory table? Inventory class instance associated with this item.
|
|
---@return boolean infoRetrievalSuccess # False if `GetItemInfo()` returned nil.
|
|
function ItemInfo:Get(itemIdentifier, itemInfoTable, reinitialize, initialize, forceIntoLocalGameCache, inventory)
|
|
assert(type(itemInfoTable) == "table", "ItemInfo:Get(): itemInfoTable must be a table")
|
|
-- Bagshui:PrintDebug(itemIdentifier)
|
|
|
|
local itemName, itemTexture, itemString, itemStringFromGetItemInfo, itemQuality, itemMinLevel, itemType, itemSubtype, itemMaxStackCount, itemEquipLocation, itemId, itemLinkWithoutRandomSuffix, itemBaseName, itemSuffixName
|
|
|
|
local item = itemInfoTable
|
|
|
|
local itemAlreadyInitialized = false
|
|
|
|
-- Load the item into the local game cache if required
|
|
if forceIntoLocalGameCache then
|
|
self:LoadItemIntoLocalGameCache(itemIdentifier)
|
|
end
|
|
|
|
-- When the itemInfoTable is empty, it always makes sense to initialize.
|
|
if initialize or (item.id == nil and item.name == nil and item.itemLink == nil) then
|
|
self:InitializeItem(item)
|
|
itemAlreadyInitialized = true
|
|
end
|
|
|
|
-- Attempt initial item information retrieval.
|
|
-- This is all done before anything else to avoid wiping the cache entry if we
|
|
-- get nil values from GetItemInfo().
|
|
-- We can only call GetItemInfo if there's an item in the slot.
|
|
if itemIdentifier ~= nil then
|
|
-- Get the various permutations of information we need from the item identifier, if it's a link or item:#:#:#:# string.
|
|
if type(itemIdentifier) == "string" then
|
|
-- itemStringGeneric currently isn't utilized in Bagshui
|
|
itemString, itemId, _, itemLinkWithoutRandomSuffix = self:ParseItemLink(itemIdentifier)
|
|
end
|
|
|
|
-- Call and store GetItemInfo results
|
|
-- itemString can be nil if itemIdentifier was numeric or ParseItemLink couldn't get results,
|
|
-- so try with the original identifier in that case.
|
|
itemName, itemStringFromGetItemInfo, itemQuality, itemMinLevel, itemType, itemSubtype, itemMaxStackCount, itemEquipLocation, itemTexture =
|
|
_G.GetItemInfo(itemString or itemIdentifier)
|
|
|
|
-- Post-server query item identifier processing.
|
|
if not itemString and itemStringFromGetItemInfo then
|
|
-- The given itemIdentifier was just an ID, but now we have the itemString.
|
|
itemString, itemId, _, itemLinkWithoutRandomSuffix = self:ParseItemLink(itemStringFromGetItemInfo)
|
|
end
|
|
|
|
-- When GetItemInfo returns nil there's (almost) nothing more we can do.
|
|
if itemName == nil then
|
|
Bagshui:PrintDebug("GetItemInfo() returned nil - bailing")
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- If we make it this far, it's safe to proceed.
|
|
|
|
-- Start fresh if allowed to do so.
|
|
-- This is separate from the previous initialization because for Inventory cache
|
|
-- entries, we don't want to wipe them until a successful call to GetItemInfo()
|
|
-- has been made. Without this protection, cache entries can be wiped at startup
|
|
-- before the client is getting answers from the server and opening the Inventory
|
|
-- at that point will be like a first-run experience instead of instantly being
|
|
-- able to see what was cached from the previous session. It's not a HUGE deal,
|
|
-- but preserving the cache does make things feel better.
|
|
if reinitialize and not itemAlreadyInitialized then
|
|
-- Bagshui:PrintDebug("re-initialized " .. tostring(item.bagNum) .. ":" .. tostring(item.slotNum))
|
|
self:InitializeItem(item)
|
|
end
|
|
|
|
-- Add item information that will be applicable regardless of whether this is
|
|
-- an empty slot (or nil'd if it's an empty slot).
|
|
|
|
item.itemString = itemString -- Must be allowed to be nil for empty slot identification.
|
|
item.id = itemId or BS_ITEM_SKELETON.id
|
|
item.texture = itemTexture or BS_ITEM_SKELETON.texture
|
|
item.bagshuiInventoryType = (inventory and inventory.inventoryType or BS_ITEM_SKELETON.bagshuiInventoryType) -- Item location (Bags, Bank, etc).
|
|
|
|
-- itemStringGeneric is disabled -- see BS_ITEM_SKELETON in Bagshui.lua for details.
|
|
-- To restore this, bring back:
|
|
-- - local itemStringGeneric at top of function.
|
|
-- - Replace _ with itemStringGeneric in both calls to BsItemInfo:ParseItemLink above.
|
|
--item.itemStringGeneric = (itemStringGeneric ~= itemString) and itemStringGeneric or nil
|
|
|
|
-- There's an item, so we can proceed with populating all the item info.
|
|
if item.itemString ~= nil then
|
|
-- GetItemInfo() results.
|
|
|
|
item.name = itemName or BS_ITEM_SKELETON.name
|
|
-- EngInventory/EngBags had an "unknown" quality value of -1 but that doesn't
|
|
-- seem likely to occur (or particularly relevant to convey?), so let's just
|
|
-- use the default quality color if we somehow didn't get one.
|
|
item.quality = itemQuality or BS_ITEM_SKELETON.quality
|
|
item.minLevel = itemMinLevel or BS_ITEM_SKELETON.minLevel
|
|
item.type = itemType or BS_ITEM_SKELETON.type
|
|
item.subtype = itemSubtype or BS_ITEM_SKELETON.subtype
|
|
item.maxStackCount = itemMaxStackCount or BS_ITEM_SKELETON.maxStackCount
|
|
item.equipLocation = itemEquipLocation or BS_ITEM_SKELETON.equipLocation
|
|
|
|
-- Apply any property overrides from Config\ItemFixes.lua.
|
|
if Bagshui.config.ItemFixes[item.id] then
|
|
for property, value in pairs(Bagshui.config.ItemFixes[item.id]) do
|
|
if BS_ITEM_SKELETON[property] then
|
|
item[property] = value
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Store localized versions of information.
|
|
if item.equipLocation and string.len(item.equipLocation) > 0 then
|
|
item.equipLocationLocalized = _G[item.equipLocation] or BS_ITEM_SKELETON.equipLocationLocalized
|
|
-- This is also the best place to add the sortable version of equipLocation.
|
|
item.equipLocationSort = BS_INVENTORY_EQUIP_LOCATION_SORT_ORDER[item.equipLocation]
|
|
or BS_ITEM_SKELETON.equipLocationSort
|
|
end
|
|
item.qualityLocalized = _G[string.format("ITEM_QUALITY%d_DESC", item.quality)]
|
|
or BS_ITEM_SKELETON.qualityLocalized
|
|
|
|
-- If this is an item with a random suffix, figure out what it is.
|
|
-- This will be used for reversed name sorting so that "Amazing Helmet of the Bear"
|
|
-- can be sorted as "Helmet Amazing Bear the of" instead of "Bear the of Helmet Amazing".
|
|
if itemLinkWithoutRandomSuffix ~= nil then
|
|
itemBaseName = _G.GetItemInfo(itemLinkWithoutRandomSuffix)
|
|
itemSuffixName = BsUtil.Trim(string.gsub(itemName, itemBaseName, ""))
|
|
end
|
|
item.baseName = itemBaseName or BS_ITEM_SKELETON.baseName
|
|
item.suffixName = itemSuffixName or BS_ITEM_SKELETON.suffixName
|
|
|
|
-- Load tooltip.
|
|
self:GetTooltip(item, inventory)
|
|
end -- itemString exists
|
|
|
|
-- Construct an item link if the itemIdentifier wasn't one.
|
|
-- itemLink must be allowed to be nil for empty slot identification.
|
|
if itemString ~= nil and not string.find(tostring(itemIdentifier), "^|") then
|
|
local itemQualityColor = _G.ITEM_QUALITY_COLORS[item.quality or 1].hex
|
|
item.itemLink = itemQualityColor .. "|H" .. itemString .. "|H[" .. (itemName or L.Unknown) .. "]|H|r"
|
|
else
|
|
item.itemLink = itemIdentifier
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- Set all values of an inventory cache item to default.
|
|
--- It's up to calling functions to manage any of the properties in
|
|
--- BS_ITEM_PROTECTED_PROPERTIES when they already have values.
|
|
---@param itemInfoTable table Table that will be filled with item information.
|
|
---@param initializeProtected boolean? Reset properties protected by `BS_ITEM_PROTECTED_PROPERTIES`.
|
|
---@param validate boolean? Never overwrite anything. In this mode, missing properties are filled and anything not in `BS_ITEM_SKELETON` is removed.
|
|
function ItemInfo:InitializeItem(itemInfoTable, initializeProtected, validate, temp)
|
|
-- Existing property resets/missing property filling.
|
|
for itemKey, defaultValue in pairs(BS_ITEM_SKELETON) do
|
|
if
|
|
(itemInfoTable[itemKey] == nil and not BS_ITEM_NIL_PROPERTIES[itemKey])
|
|
or (
|
|
-- Don't reset properties with values during validation.
|
|
not validate
|
|
-- Never reset a protected property unless forced.
|
|
and not (BS_ITEM_PROTECTED_PROPERTIES[itemKey] and not initializeProtected)
|
|
)
|
|
then
|
|
if type(defaultValue) == "table" then
|
|
itemInfoTable[itemKey] = BsUtil.TableCopy(defaultValue)
|
|
else
|
|
itemInfoTable[itemKey] = defaultValue
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Validation only: remove extraneous properties.
|
|
if validate then
|
|
for itemKey in next, itemInfoTable do
|
|
if BS_ITEM_SKELETON[itemKey] == nil then
|
|
rawset(itemInfoTable, itemKey, nil)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Set the proper cache values for an empty slot item.
|
|
---@param item table Filled by `ItemInfo:Get()`.
|
|
---@param isEmptySlotStack boolean? true for empty slot stack proxy entries.
|
|
function ItemInfo:InitializeEmptySlotItem(item, isEmptySlotStack)
|
|
item.id = 0
|
|
-- Using 0/1 for ease of sorting.
|
|
item.emptySlot = 1
|
|
-- Add "[Profession Bag Type]" to the name when needed.
|
|
item.name = (isEmptySlotStack and item.bagType ~= BsGameInfo.itemSubclasses.Container.Bag)
|
|
and string.format(L.Suffix_EmptySlot, item.bagType)
|
|
or L.ItemPropFriendly_emptySlot
|
|
item.subtype = item.bagType
|
|
item.quality = -1
|
|
item.charges = -1
|
|
item.texture = nil
|
|
item.readable = nil
|
|
if not item._bagsRepresented then
|
|
item._bagsRepresented = {}
|
|
else
|
|
BsUtil.TableClear(item._bagsRepresented)
|
|
end
|
|
end
|
|
|
|
--- `GetItemInfo()` in Vanilla doesn't accept the full item links returned by `GetContainerItemLink()`,
|
|
--- so we need to parse out just the `item:itemId:enchantId:suffixId:uniqueId` part.
|
|
---
|
|
--- While we're doing that, we might as well pull out the item ID and the generic item link,
|
|
--- which is the same as the item link with enchantId and uniqueId set to 0, (even though we're not using it).
|
|
---
|
|
--- If applicable, also figure out the version of the item string associated with the base item that
|
|
--- doesn't have a the random suffix (i.e. "of the Bear") for use during reverse name sorting
|
|
--- (see comment about `itemLinkWithoutRandomSuffix` in `ItemInfo:Get()`).
|
|
---
|
|
--- Reference: https://warcraft.wiki.gg/index.php?title=ItemLink&oldid=4718845
|
|
---@param itemLink string Item link or item string.
|
|
---@return string? itemString
|
|
---@return number? itemId
|
|
---@return string? itemStringGeneric
|
|
---@return string? itemStringWithoutRandomSuffix
|
|
function ItemInfo:ParseItemLink(itemLink)
|
|
if not itemLink then
|
|
return
|
|
end
|
|
local itemString, itemStringGeneric, itemStringWithoutRandomSuffix
|
|
local found, _, itemId, enchantId, suffixId, uniqueId =
|
|
string.find(tostring(itemLink), "item:(%d+):(%d+):(%d+):(%d+)")
|
|
if found then
|
|
local itemFormatString = "item:%s:%s:%s:%s"
|
|
itemString = string.format(itemFormatString, itemId, enchantId, suffixId, uniqueId)
|
|
itemStringGeneric = string.format(itemFormatString, itemId, "0", suffixId, "0")
|
|
if suffixId ~= "0" then
|
|
itemStringWithoutRandomSuffix = string.format(itemFormatString, itemId, "0", "0", "0")
|
|
end
|
|
end
|
|
return itemString, tonumber(itemId), itemStringGeneric, itemStringWithoutRandomSuffix
|
|
end
|
|
|
|
--- Store item's tooltip on the item.tooltip property as a single string.
|
|
--- Also parse charges from the tooltip if possible.
|
|
---@param item table Item information table in the format of BS_ITEM_SKELETON.
|
|
---@param inventory table? Inventory class, if applicable.
|
|
---@param forceItemString boolean? Ignore bagNum/slotNum properties on `item` and use `itemString`.
|
|
function ItemInfo:GetTooltip(item, inventory, forceItemString)
|
|
-- Reset charges first since this is where they're picked up (when SuperWoW isn't loaded).
|
|
if not BS_SUPER_WOW_LOADED then
|
|
item.charges = 0
|
|
end
|
|
|
|
item.tooltip = ""
|
|
item.lockPickable = BS_ITEM_SKELETON.lockPickable
|
|
item.openable = BS_ITEM_SKELETON.openable
|
|
|
|
-- Pull the tooltip text into our hidden tooltip.
|
|
BsHiddenTooltip:SetOwner(_G.WorldFrame, "ANCHOR_NONE") -- Need to clear lines first.
|
|
self:LoadTooltip(BsHiddenTooltip, item, inventory, forceItemString)
|
|
|
|
-- Loop through all lines of tooltip and concatenate.
|
|
-- This is also where we can figure out if an item has charges.
|
|
local ttTextFrame, ttText
|
|
local ttBagshuiData = BsHiddenTooltip.bagshuiData
|
|
local chargesFound, chargeCount
|
|
-- Lines
|
|
for ttLineNum = 2, BsHiddenTooltip:NumLines() do
|
|
-- Left/Right
|
|
-- Don't use _ as a placeholder here since it's used inside the loop.
|
|
for __, lr in ipairs(ttBagshuiData.textFieldsPerLine) do
|
|
ttTextFrame = _G[ttBagshuiData.name .. "Text" .. lr .. ttLineNum]
|
|
if ttTextFrame ~= nil and ttTextFrame:IsVisible() then
|
|
ttText = ttTextFrame:GetText()
|
|
if ttText ~= nil then
|
|
-- Append to tooltip.
|
|
ttText = string.gsub(ttText, "^%s+$", "")
|
|
item.tooltip = item.tooltip .. BS_INVENTORY_TOOLTIP_JOIN_CHARACTERS[lr] .. ttText
|
|
|
|
-- Check whether openable or lock-pickable.
|
|
if ttText == L.TooltipIdentifier_Openable then
|
|
item.openable = 1
|
|
end
|
|
if ttText == L.TooltipIdentifier_Locked then
|
|
item.lockPickable = 1
|
|
end
|
|
|
|
-- Parse charges and store, but don't overwrite if we already know them.
|
|
if not BS_SUPER_WOW_LOADED and item.charges == 0 then
|
|
chargesFound, _, chargeCount = string.find(ttText, L.TooltipParse_Charges)
|
|
if chargesFound then
|
|
item.charges = tonumber(chargeCount)
|
|
end
|
|
end
|
|
end -- ttText exists
|
|
end -- ttTextFrame exists
|
|
end -- Left/Right
|
|
end -- Lines
|
|
|
|
item.tooltip = BsUtil.Trim(item.tooltip)
|
|
|
|
-- Might as well update usable status while we have the tooltip loaded.
|
|
self:CacheUsableStatusFromHiddenTooltip(item, false)
|
|
end
|
|
|
|
--- Load the tooltip for the given item into the provided `tooltipFrame`.
|
|
--- Normally we can use `SetBagItem()`, but some containers need to use `SetInventoryItem()` instead.
|
|
---@param tooltipFrame table WoW UI tooltip frame created with `CreateFrame("GameTooltip" ... )`.
|
|
---@param item table<string,string|number>|string Item information table populated by ItemInfo:Get() or an itemString ("item:#:#:#:#").
|
|
---@param inventory table? Inventory class instance, if applicable.
|
|
---@param forceItemString boolean? Ignore bagNum/slotNum properties on `item` and use `itemString`.
|
|
---@return number|nil|boolean hasCooldown Is the item on cooldown?
|
|
---@return number|nil repairCost If the item is damaged, value in copper to repair.
|
|
function ItemInfo:LoadTooltip(tooltipFrame, item, inventory, forceItemString)
|
|
local hasCooldown, repairCost
|
|
|
|
-- Wipe the tooltip first, just to be safe.
|
|
tooltipFrame:ClearLines()
|
|
|
|
-- Determine whether the tooltip needs to be loaded using the itemString (via SetHyperlink()).
|
|
-- There are multiple reasons this can occur:
|
|
--
|
|
-- 1. A string or number was passed instead of a table.
|
|
local itemString = type(item) == "string" and item
|
|
or type(item) == "number" and ("item:" .. item .. ":0:0:0:0:0:0:0:0")
|
|
or nil --[[@as string|number|nil]]
|
|
--
|
|
-- 2a. An an item table was passed but bagNum and slotNum are both the default, meaning we
|
|
-- can't use SetInventoryItem()/SetBagItem().
|
|
-- 2b. Offline mode, where the Set*Item() functions aren't available.
|
|
-- 2C. forceItemString is set.
|
|
if
|
|
type(item) == "table"
|
|
and (
|
|
(item.bagNum == BS_ITEM_SKELETON.bagNum and item.slotNum == BS_ITEM_SKELETON.slotNum)
|
|
or (inventory and not inventory.online)
|
|
or forceItemString
|
|
)
|
|
then
|
|
itemString = item.itemString
|
|
end
|
|
|
|
-- When itemString was given a value for one of the reasons above, that means
|
|
-- we should use SetHyperlink().
|
|
if itemString then
|
|
tooltipFrame:SetHyperlink(itemString)
|
|
elseif type(item) == "table" then
|
|
-- Inventory configurations can provide a function that translates the item's slotNum
|
|
-- from the sequential 0-N bag slot to the global inventory slot number when
|
|
-- they need to use SetInventoryItem() instead of SetBagItem().
|
|
if inventory and inventory.primaryContainer.id == item.bagNum and inventory.getInventorySlotFunction then
|
|
-- SetInventoryItem() returns hasItem, hasCooldown, repairCost.
|
|
_, hasCooldown, repairCost =
|
|
tooltipFrame:SetInventoryItem("player", inventory.getInventorySlotFunction(item.slotNum))
|
|
else
|
|
hasCooldown, repairCost = tooltipFrame:SetBagItem(item.bagNum, item.slotNum)
|
|
end
|
|
end
|
|
|
|
return hasCooldown, repairCost
|
|
end
|
|
|
|
--- Fill the given tooltip with a list of all item properties.
|
|
---@param item table Filled by `ItemInfo:Get()`.
|
|
---@param tooltip table WoW UI tooltip.
|
|
function ItemInfo:AddTooltipInfo(item, tooltip)
|
|
Bagshui:AddTooltipLine(tooltip, nil, L.ItemProperties, true)
|
|
-- Iterate all item properties.
|
|
for _, itemPropertyFriendly, itemPropertyValue, itemPropertyDisplay in
|
|
self:ItemPropertyValuesForDisplay(item, BS_ITEM_INFO_DISPLAY_TYPE.TOOLTIP)
|
|
do
|
|
if itemPropertyValue ~= nil then
|
|
Bagshui:AddTooltipLine(tooltip, itemPropertyDisplay, string.format(L.Symbol_Colon, itemPropertyFriendly))
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Ensure GetItemInfo will work for an item by forcing it into the local game cache via hidden tooltip load.
|
|
---@param itemIdentifier string|number|nil itemString ("item:#:#:#:#") or item number.
|
|
function ItemInfo:LoadItemIntoLocalGameCache(itemIdentifier)
|
|
if not itemIdentifier or itemIdentifier == 0 then
|
|
return
|
|
end
|
|
local itemString
|
|
if type(itemIdentifier) == "number" or (type(itemIdentifier) == "string" and tonumber(itemIdentifier)) then
|
|
-- Extra 0s seem to sometimes be necessary to make SetHyperlink work, and they never have hurt during testing.
|
|
itemString = "item:" .. itemIdentifier .. ":0:0:0:0:0:0:0:0"
|
|
else
|
|
itemString = self:ParseItemLink(itemIdentifier)
|
|
end
|
|
if itemString then
|
|
BsHiddenTooltip:SetHyperlink(itemString)
|
|
end
|
|
end
|
|
|
|
--- Check whether the given Bagshui item is usable by the current character.
|
|
--- This originally determined status by processing item properties and parsing
|
|
--- the tooltip to compare against character level, class, and skills.
|
|
--- That worked great until it didn't (Fist Weapons totally broke it), and
|
|
--- it was completely reworked to use the new method in `ItemInfo:CacheUsableStatusFromHiddenTooltip()`.
|
|
---@param item table<string,string|number> Bagshui Inventory cache entry.
|
|
---@return boolean usable
|
|
---@return boolean? alreadyKnown
|
|
function ItemInfo:IsUsable(item)
|
|
-- When there's no information about the item or character, assume it IS usable.
|
|
if not item.id or item.id == 0 then
|
|
return true
|
|
end
|
|
|
|
-- Update cache if needed.
|
|
if
|
|
not self.itemUsableStatusCache[item.id]
|
|
or not self.itemUsableStatusCacheTimestamps[item.id]
|
|
or self.itemUsableStatusCacheTimestamps[item.id] < self.itemUsableStatusMinTimestamp
|
|
then
|
|
self:CacheUsableStatusFromHiddenTooltip(item, true)
|
|
end
|
|
|
|
-- Determine status.
|
|
if self.itemUsableStatusCache[item.id] == BS_ITEM_USABLE.NO then
|
|
return false
|
|
elseif self.itemUsableStatusCache[item.id] == BS_ITEM_USABLE.KNOWN then
|
|
return true, true
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- Determine whether the given item is usable based on information in its tooltip.
|
|
--- The result will be stored in `ItemInfo.itemUsableStatusCache`.
|
|
---
|
|
--- It's built this way so the function can be called from `ItemInfo:GetTooltip()`
|
|
--- and `ItemInfo:IsUsable()`. By calling from the former, we can pre-cache
|
|
--- usable status for many items and avoid loading the tooltip multiple times.
|
|
---
|
|
--- Based on techniques from pfUI's `unusable:UpdateSlot()` and libtipscan `findColor()`.
|
|
---@param item table Item information table in the format of BS_ITEM_SKELETON.
|
|
---@param needToLoadTooltip boolean? `true` if the item's tooltip is NOT currently loaded into `BsHiddenTooltip`.
|
|
function ItemInfo:CacheUsableStatusFromHiddenTooltip(item, needToLoadTooltip)
|
|
local usable
|
|
|
|
-- Already known check.
|
|
-- Unfortunately the "Already Known" text in the tooltip is red, not green,
|
|
-- so we need to actually look for the text itself.
|
|
if item.tooltip and string.find(item.tooltip, L.TooltipParse_AlreadyKnown) then
|
|
-- Bagshui:PrintDebug(item.name .. " already known")
|
|
usable = BS_ITEM_USABLE.KNOWN
|
|
end
|
|
|
|
-- Look for red text in the tooltip, which indicates it's unusable.
|
|
-- We're going to stop at the first blank line because recipes indicate usability
|
|
-- of the recipe itself, then an empty line, followed by information about the item
|
|
-- created by the recipe, which includes whether the item is usable. We only want
|
|
-- to know about the recipe's usability.
|
|
if not usable then
|
|
if needToLoadTooltip then
|
|
self:LoadTooltip(BsHiddenTooltip, item)
|
|
end
|
|
|
|
local ttTextFrame, ttText, leftText, rightText, hasRedText, r, g, b
|
|
local ttBagshuiData = BsHiddenTooltip.bagshuiData
|
|
|
|
-- This duplicates code and logic in Inventory:ItemButton_OnEnter() as well
|
|
-- as ItemInfo:GetTooltip(), so it would be nice to refactor eventually.
|
|
for ttLineNum = 1, BsHiddenTooltip:NumLines() do
|
|
leftText = nil
|
|
rightText = nil
|
|
hasRedText = false
|
|
-- Left/Right
|
|
for i, lr in ipairs(ttBagshuiData.textFieldsPerLine) do
|
|
ttTextFrame = _G[ttBagshuiData.name .. "Text" .. lr .. ttLineNum]
|
|
if ttTextFrame ~= nil and ttTextFrame:IsVisible() then
|
|
ttText = ttTextFrame:GetText()
|
|
|
|
if ttText ~= nil and not string.find(ttText, "^" .. BS_NEWLINE) then
|
|
if i == 1 then
|
|
leftText = BsUtil.Trim(ttText)
|
|
else
|
|
rightText = BsUtil.Trim(ttText)
|
|
end
|
|
end
|
|
|
|
r, g, b = ttTextFrame:GetTextColor()
|
|
r, g, b = BsUtil.Round(r, 1), BsUtil.Round(g, 1), BsUtil.Round(b, 1)
|
|
if r == _G.RED_FONT_COLOR.r and g == _G.RED_FONT_COLOR.g and b == _G.RED_FONT_COLOR.b then
|
|
-- Just track whether there's red text until we're sure it's
|
|
-- not a blank red line (hey, it could happen).
|
|
hasRedText = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- We found a blank line and need to stop.
|
|
if string.len(leftText or "") == 0 and string.len(rightText or "") == 0 then
|
|
break
|
|
end
|
|
|
|
-- Once we're sure it's not a blank line, we can take action if it's not usable.
|
|
if hasRedText then
|
|
usable = BS_ITEM_USABLE.NO
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update cache, defaulting to usable if we haven't found a reason it's not.
|
|
self.itemUsableStatusCache[item.id] = usable or BS_ITEM_USABLE.YES
|
|
self.itemUsableStatusCacheTimestamps[item.id] = _G.GetTime()
|
|
end
|
|
|
|
--- Call this to require all item usability statues to be updated.
|
|
function ItemInfo:InvalidateUsableCache()
|
|
self.itemUsableStatusMinTimestamp = _G.GetTime()
|
|
end
|
|
|
|
--- Is the given item a bag?
|
|
---@param item table Inventory cache entry.
|
|
---@return boolean
|
|
function ItemInfo:IsContainer(item)
|
|
return (type(item) == "table" and (item.type == L.Container or item.type == L.Quiver))
|
|
end
|
|
|
|
--- Wrap an item's name in the appropriate color escape sequence for its quality level.
|
|
---@param item table<string,string|number> Item information table populated by ItemInfo:Get().
|
|
---@return string
|
|
function ItemInfo:GetQualityColoredName(item)
|
|
if type(item) ~= "table" then
|
|
return L.Unknown
|
|
end
|
|
return _G.ITEM_QUALITY_COLORS[item.quality or 1].hex
|
|
.. ((string.len(item.name or "") > 0) and item.name or L.Unknown)
|
|
.. FONT_COLOR_CODE_CLOSE
|
|
end
|
|
|
|
--- Obtain a sorted array of either the full list of PeriodicTable sets or
|
|
--- those pertaining to the given item.
|
|
---@param itemOrItemLink table<string,string|number>|string|number? Item information table populated by ItemInfo:Get() or item link.
|
|
---@return table
|
|
function ItemInfo:GetPeriodicTableSets(itemOrItemLink)
|
|
-- When an item info table is provided, grab the item link.
|
|
if type(itemOrItemLink) == "table" then
|
|
itemOrItemLink = itemOrItemLink.itemLink
|
|
end
|
|
|
|
-- We've just seen this item and already know its sets.
|
|
if
|
|
(itemOrItemLink and itemOrItemLink == self.periodicTableSetCacheItemLink)
|
|
or (not itemOrItemLink and self.periodicTableSetCacheItemLink == "~")
|
|
then
|
|
return self.periodicTableSetCache
|
|
end
|
|
|
|
-- Reset the cache and update the cached item identifier.
|
|
BsUtil.TableClear(self.periodicTableSetCache)
|
|
self.periodicTableSetCacheItemLink = (itemOrItemLink or "~")
|
|
|
|
-- We need to directly access PeriodicTable's k set storage structure because
|
|
-- it doesn't seem to provide any way to request the listing of built-in sets.
|
|
for setName, _ in pairs(Bagshui.libs.PT.k.customsets) do
|
|
if not itemOrItemLink or (itemOrItemLink and Bagshui.libs.PT:ItemInSet(itemOrItemLink, setName)) then
|
|
table.insert(self.periodicTableSetCache, setName)
|
|
end
|
|
end
|
|
|
|
table.sort(self.periodicTableSetCache)
|
|
|
|
return self.periodicTableSetCache
|
|
end
|
|
|
|
--- Create a string consisting of itemString, count, and item location that allows
|
|
--- an item to be (almost) uniquely identified. Used by the "shadow ID" system in
|
|
--- the inventory cache -- see comment above the definition of shadowId in
|
|
--- Inventory.Cache.lua for detail.
|
|
---@param item table<string,string|number> Item information table populated by ItemInfo:Get().
|
|
---@param bagNumOverride number? Get the unique ID as if the item is in a different container.
|
|
---@param slotNumOverride number? Get the unique ID as if the item is in a different bag slot.
|
|
---@return string
|
|
function ItemInfo:GetUniqueItemId(item, bagNumOverride, slotNumOverride)
|
|
return (bagNumOverride or item.bagNum or "?")
|
|
.. ":"
|
|
.. (slotNumOverride or item.slotNum or "?")
|
|
.. ":"
|
|
.. (item.itemString or "?")
|
|
.. ":"
|
|
.. (item.count or 0)
|
|
.. ":"
|
|
.. (item.bagshuiInventoryType or "?")
|
|
end
|
|
|
|
--- Display the Item Information Window for the given item
|
|
---@param item table<string,string|number> Item information table populated by ItemInfo:Get().
|
|
function ItemInfo:Open(item)
|
|
self.window:Open(item)
|
|
end
|
|
|
|
local arbitraryItemInfo = {}
|
|
function ItemInfo:OpenWithArbitraryItem(itemIdentifier)
|
|
if self.lastArbitraryItem == itemIdentifier then
|
|
self.arbitraryItemTries = (self.arbitraryItemTries or 0) + 1
|
|
else
|
|
self.arbitraryItemTries = 0
|
|
end
|
|
self.lastArbitraryItem = itemIdentifier
|
|
if not self:Get(itemIdentifier, arbitraryItemInfo, true, true, true) then
|
|
if self.arbitraryItemTries < 5 then
|
|
Bagshui:QueueClassCallback(self, self.OpenWithArbitraryItem, 0.75, nil, itemIdentifier)
|
|
else
|
|
Bagshui:PrintError(string.format(L.Error_ItemNotFound, tostring(itemIdentifier)))
|
|
end
|
|
return
|
|
end
|
|
|
|
self:Open(arbitraryItemInfo)
|
|
end
|
|
|
|
--- Construct the code examples for the info window.
|
|
---@param ruleFunctionExamples string[] Rule functions. If `%s` is found, the item property value will be substituted.
|
|
---@param itemPropertyValue string|number? Value to fill if %s is present in the rule function.
|
|
---@param itemPropertyFriendly string User-facing localized name for the item property, used if `itemPropertyValue` is nil.
|
|
---@return string
|
|
function ItemInfo:BuildRuleFunctionExampleText(ruleFunctionExamples, itemPropertyValue, itemPropertyFriendly)
|
|
local ret = ""
|
|
for _, ruleFunctionExample in ipairs(ruleFunctionExamples) do
|
|
-- The substitution text for rule function parameters is either the actual value with
|
|
-- double quotes escaped or `<Friendly Property Name>` if the property doesn't have a value.
|
|
local ruleFunctionParameter = itemPropertyValue ~= nil and string.gsub(tostring(itemPropertyValue), '"', '\\"')
|
|
or ("<" .. tostring(itemPropertyFriendly) .. ">")
|
|
-- Replace %s or %d in the example with the parameter value.
|
|
if string.find(ruleFunctionExample, "%%s") then
|
|
ruleFunctionExample = string.format(ruleFunctionExample, tostring(ruleFunctionParameter))
|
|
end
|
|
-- Add to info text.
|
|
ret = ret .. BS_FONT_COLOR.CODE_EXAMPLE .. ruleFunctionExample .. FONT_COLOR_CODE_CLOSE .. BS_NEWLINE
|
|
end
|
|
return ret
|
|
end
|
|
|
|
--- Get an iterator function that will return item properties in the order of `BS_ITEM_PROPERTIES_SORTED`.
|
|
---@param item table<string,string|number> Item information table populated by ItemInfo:Get().
|
|
---@param displayType BS_ITEM_INFO_DISPLAY_TYPE?
|
|
---@return function iterableFunction function with the following return values, in order:
|
|
--- ```
|
|
--- ---@return string propertyName # Actual property name.
|
|
--- ---@return string propertyDisplayName # Friendly localized property name for display.
|
|
--- ---@return any propertyValue # Actual property value.
|
|
--- ---@return any propertyDisplayValue # Same as `propertyValue`, but will be the localized "(No Value)" string if `propertyValue` is empty.
|
|
--- ```
|
|
function ItemInfo:ItemPropertyValuesForDisplay(item, displayType)
|
|
self:BuildSortedItemTemplatePropertyList()
|
|
|
|
-- These upvalues are needed for the iterator function.
|
|
local i = 0
|
|
local realtimeItemProperties = BS_REALTIME_ITEM_INFO_PROPERTIES
|
|
local L = L
|
|
|
|
return function()
|
|
i = i + 1
|
|
|
|
local itemProperty = BS_ITEM_PROPERTIES_SORTED[i]
|
|
|
|
local itemPropertyFriendly = itemProperty and L["ItemPropFriendly_" .. itemProperty]
|
|
|
|
local itemPropertyValue = (
|
|
(
|
|
not (
|
|
displayType == BS_ITEM_INFO_DISPLAY_TYPE.TOOLTIP
|
|
and item.bagshuiInventoryType == nil
|
|
and BS_ITEM_PROPERTIES_SUPPRESSED_IN_TOOLTIP_OUTSIDE_INVENTORY[itemProperty]
|
|
) and itemProperty
|
|
)
|
|
and (realtimeItemProperties[itemProperty] and realtimeItemProperties[itemProperty](item, displayType) or (item[itemProperty] ~= nil and item[itemProperty] ~= "") and tostring(
|
|
item[itemProperty]
|
|
) or nil)
|
|
or nil
|
|
)
|
|
|
|
-- Display value is the actual value if it exists or "(No Value)" if not.
|
|
-- Will be truncated unless it's for text display.
|
|
local itemPropertyDisplay = itemPropertyValue
|
|
and (type(itemPropertyValue) ~= "table" and (NORMAL_FONT_COLOR_CODE .. (displayType ~= Bagshui.environment.BS_ITEM_INFO_DISPLAY_TYPE.TEXT and BsUtil.TruncateString(
|
|
tostring(itemPropertyValue),
|
|
35
|
|
) or tostring(itemPropertyValue)) .. FONT_COLOR_CODE_CLOSE) or "")
|
|
or GRAY_FONT_COLOR_CODE .. L.NoValue .. FONT_COLOR_CODE_CLOSE
|
|
|
|
if itemPropertyValue == "" then
|
|
itemPropertyValue = nil
|
|
end
|
|
|
|
return itemProperty, itemPropertyFriendly, itemPropertyValue, itemPropertyDisplay
|
|
end
|
|
end
|
|
|
|
--- Fill BS_ITEM_PROPERTIES_SORTED with a list of item properties that have a corresponding
|
|
--- rule function, sorted in friendly (localized) name order.
|
|
function ItemInfo:BuildSortedItemTemplatePropertyList()
|
|
-- Only do this once.
|
|
if not Bagshui.environment.BS_ITEM_PROPERTIES_SORTED then
|
|
local itemPropertiesSorted = {}
|
|
-- Only add properties that have a corresponding rule function.
|
|
for prop, _ in pairs(BS_ITEM_PROPERTIES_TO_FUNCTIONS) do
|
|
table.insert(itemPropertiesSorted, prop)
|
|
end
|
|
-- Sort the list.
|
|
table.sort(itemPropertiesSorted, function(itmA, itmB)
|
|
return L["ItemPropFriendly_" .. itmA] < L["ItemPropFriendly_" .. itmB]
|
|
end)
|
|
-- Save to Bagshui environment.
|
|
Bagshui.environment.BS_ITEM_PROPERTIES_SORTED = itemPropertiesSorted
|
|
end
|
|
end
|
|
|
|
--- Just need to do this little bit after initialization because the UI classes
|
|
--- aren't loaded until well after ItemInfo.
|
|
function ItemInfo:Init()
|
|
-- Add /bagshui info
|
|
-- The Character parameter is handled here for convenience.
|
|
local itemInfoSlashCache = {}
|
|
BsSlash:AddHandler("Info", function(tokens)
|
|
if not tokens[2] then
|
|
-- Character may be added in a future update.
|
|
BsSlash:PrintHandlers({ L.Group, L.ItemId, L.Location }, "Info")
|
|
elseif BsGameInfo:HandleInfoSlash(tokens) then
|
|
-- Let GameInfo handle its own stuff.
|
|
return
|
|
elseif BsUtil.MatchLocalizedOrNon(tokens[2], "itemid") or BsUtil.MatchLocalizedOrNon(tokens[2], "help") then
|
|
Bs:PrintBare(L.Slash_Help_ItemInfo)
|
|
else
|
|
self:OpenWithArbitraryItem(tokens[2])
|
|
end
|
|
end)
|
|
|
|
-- Get the UI ready.
|
|
self.window = Bagshui.prototypes.ScrollableTextWindow:New({
|
|
name = "ItemInfo",
|
|
title = "Item Info", -- Will be updated to the name of the item in Open().
|
|
readOnly = true,
|
|
width = 400,
|
|
height = 650,
|
|
selectAllOnFocus = false,
|
|
})
|
|
|
|
--- ScrollableTextWindow override for item info display.
|
|
---@param item table<string,string|number> Item information table populated by ItemInfo:Get().
|
|
function self.window:Open(item)
|
|
-- Nothing to do.
|
|
if not item then
|
|
return
|
|
end
|
|
|
|
local infoText = ""
|
|
|
|
if type(item) == "table" then
|
|
-- Build item information text.
|
|
|
|
for itemProperty, itemPropertyFriendly, itemPropertyValue, itemPropertyDisplay in
|
|
ItemInfo:ItemPropertyValuesForDisplay(item, BS_ITEM_INFO_DISPLAY_TYPE.TEXT)
|
|
do
|
|
-- Start this property with `<Friendly Property Name>: <Property Value>`.
|
|
infoText = infoText
|
|
.. string.format(L.Symbol_Colon, itemPropertyFriendly)
|
|
.. " "
|
|
.. itemPropertyDisplay
|
|
.. BS_NEWLINE
|
|
|
|
-- Add rule function examples.
|
|
local ruleFunctionExamples = BS_ITEM_PROPERTIES_TO_FUNCTIONS[itemProperty]
|
|
if type(ruleFunctionExamples) == "table" then
|
|
if type(itemPropertyValue) == "table" then
|
|
for _, val in ipairs(itemPropertyValue) do
|
|
infoText = infoText
|
|
.. ItemInfo:BuildRuleFunctionExampleText(ruleFunctionExamples, val, itemPropertyFriendly)
|
|
end
|
|
else
|
|
infoText = infoText
|
|
.. ItemInfo:BuildRuleFunctionExampleText(ruleFunctionExamples, itemPropertyValue, itemPropertyFriendly)
|
|
end
|
|
else
|
|
infoText = infoText .. GRAY_FONT_COLOR_CODE .. L.NoRuleFunction .. FONT_COLOR_CODE_CLOSE .. BS_NEWLINE
|
|
end
|
|
|
|
-- End with a final newline so that there are two newlines to separate from
|
|
-- the next property (extra newlines at the very end don't matter).
|
|
infoText = infoText .. BS_NEWLINE
|
|
end
|
|
end
|
|
|
|
-- Prepare to open the window.
|
|
if self._super.Open(self, infoText, true) == false then
|
|
return
|
|
end
|
|
|
|
-- Set title to item name.
|
|
self.uiTitle:SetText(
|
|
string.format(L.Symbol_Colon, L.BagshuiItemInformation)
|
|
.. " "
|
|
.. HIGHLIGHT_FONT_COLOR_CODE
|
|
.. tostring(item.name)
|
|
.. FONT_COLOR_CODE_CLOSE
|
|
)
|
|
|
|
-- Set item slot button in top left to item.
|
|
self.ui:AssignItemToItemButton(self.itemSlotButton, item)
|
|
self.itemSlotButton.bagshuiData.itemString = item.itemString -- This will be picked up for the tooltip.
|
|
|
|
-- Actually open the window.
|
|
-- self:Open()
|
|
end
|
|
|
|
--- ScrollableTextWindow override for initializing UI.
|
|
function self.window:InitUi()
|
|
if not self then
|
|
return
|
|
end
|
|
|
|
-- Calls ScrollableTextWindow:InitUi().
|
|
if self._super.InitUi(self) == false then
|
|
return
|
|
end
|
|
|
|
-- Add item slot button to top left of window.
|
|
self.itemSlotButton = self.ui:CreateItemSlotButton("ItemSlotButton", self.uiHeader)
|
|
self.itemSlotButton.bagshuiData.tooltipAnchorDefault = true
|
|
self.itemSlotButton.bagshuiData.noBorderScale = true
|
|
self.ui:SetItemButtonSize(self.itemSlotButton, self.uiTitle:GetHeight() + 2)
|
|
self.itemSlotButton:SetPoint("LEFT", self.uiFrame.bagshuiData.header)
|
|
-- Remove clickability and pass drag events to the window.
|
|
self.itemSlotButton:RegisterForClicks(nil)
|
|
self.itemSlotButton:RegisterForDrag("LeftButton")
|
|
self.itemSlotButton:SetScript("OnDragStart", function()
|
|
if _G.GameTooltip:IsOwned(_G.this) then
|
|
_G.GameTooltip:Hide()
|
|
end
|
|
self.uiFrame:StartMoving()
|
|
end)
|
|
self.itemSlotButton:SetScript("OnDragStop", function()
|
|
self.uiFrame:StopMovingOrSizing()
|
|
end)
|
|
|
|
-- Anchor title to item slot button.
|
|
self.uiTitle:ClearAllPoints()
|
|
self.uiTitle:SetPoint("LEFT", self.itemSlotButton, "RIGHT", 4, 0)
|
|
end
|
|
end
|
|
|
|
--- Event handling.
|
|
---@param event string Event identifier.
|
|
---@param arg1 any? Argument 1 from the event.
|
|
function ItemInfo:OnEvent(event, arg1)
|
|
if event == "ADDON_LOADED" then
|
|
self:Init()
|
|
end
|
|
end
|
|
|
|
Bagshui:RegisterEvent("ADDON_LOADED", ItemInfo)
|
|
end)
|