Files
OEVG 896f46f0d1
enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
Bagshui 1.6.4
2026-10-30 15:55:43 +00:00

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)