---@diagnostic disable: duplicate-set-field
-- **************************************************************************
-- * TitanBag.lua
-- *
-- * By: The Titan Panel Development Team
-- **************************************************************************
-- ******************************** Constants *******************************
local _G = getfenv(0);
local TITAN_BAG_ID = "Bag";
local TITAN_BUTTON = "TitanPanel" .. TITAN_BAG_ID .. "Button"
local TITAN_BAG_THRESHOLD_TABLE = {
Values = { 0.5, 0.75, 0.9 },
Colors = { HIGHLIGHT_FONT_COLOR, NORMAL_FONT_COLOR, ORANGE_FONT_COLOR, RED_FONT_COLOR },
}
local L = LibStub("AceLocale-3.0"):GetLocale(TITAN_ID, true)
--local updateTable = {TITAN_BAG_ID, TITAN_PANEL_UPDATE_BUTTON};
-- ******************************** Variables *******************************
--local AceTimer = LibStub("AceTimer-3.0")
local trace = false
local MIN_BAGS = 0
local MAX_BAGS = 0
local bag_data = {} -- to hold the user bag data
-- ******************************** Functions *******************************
-- Set so Retail and Classic can run
local GetItemNow = C_Item.GetItemInfoInstant or GetItemInfoInstant
---Determine if this is a profession bag using only instant data rather than calling server
---@param slot number
---@return boolean
local function IsProfessionBagID(slot)
-- The info needed is available using GetItemInfoInstant; only the bag slot or item id is required.
-- A LOT of info is available but we only need class and subclass here.
-- itemType : warcraft.wiki.gg/wiki/itemType
local res = false
local info, itemId, itemType, itemSubType, itemEquipLoc, itemTexture, classID, subclassID
local inv_id = C_Container.ContainerIDToInventoryID(slot)
if inv_id == nil then
-- Only works on bag and bank bags NOT backpack!
-- However the backpack is never a profession bag.
else
info = GetInventoryItemLink("player", inv_id)
if info == nil then
-- Slot likely empty, no need to process.
else
itemId, itemType, itemSubType, itemEquipLoc, itemTexture, classID, subclassID = GetItemNow(info)
if classID == 1 then -- is a container / bag
if subclassID >= 1 then
-- profession bag of some type [2 - 10] Jan 2024 (DragonFlight / Wrath / Classic Era)
-- OR soul bag [1]
res = true
else
-- is a arrow or bullet bag; only two options
end
elseif classID == 6 then -- is a 'projectile' holder
res = true
-- is a ammo bag or quiver; only two options
elseif classID == 11 then -- is a 'quiver'; Wrath and CE
res = true
-- is a ammo pouch or quiver; only two options
-- style = subclassID + 20 -- change to get local color for name
else
-- not a profession bag
end
end
end
return res
end
---Tell the UI to open / close the bags
local function ToggleBags()
if TitanGetVar(TITAN_BAG_ID, "OpenBags") then
ToggleAllBags()
else
-- User has not enabled open on click
end
end
---Collect bag info - name, slots (total, used, free), name (if available).
--- The bag name is not always available when player entering world but the required info is.
---@param id string Plugin ID
local function GetBagData(id)
--[[
The bag name is not always available when player entering world.
The user may see bag name as <unknown> until an event triggers a bag check AND the name is available.
Grabbing the total slots is available on client to determine if a bag exists and get its free / used counts.
--]]
-- 2024 Jan : Moved away from named based to id based. Allows name to come later from server
-- 2024 Aug : Removed coloring of bag name to focus on counts which is the real info.
if trace then
TitanPluginDebug(TITAN_BAG_ID, "T GetBagData"
.. " '" .. tostring(id) .. "'"
)
end
local total_slots = 0
local total_free = 0
local total_used = 0
local is_prof_bag = false
-- calculated but not used ATM
local prof_slots = 0
local prof_free = 0
local prof_used = 0
for bag_slot = MIN_BAGS, MAX_BAGS do -- assuming 0 (Backpack) will not be a profession bag
-- Ensure a blank structure exists.
-- Blanking data may seem overkill but it allows the plugin to react to events without
-- caring when they occur and it will set the bag name when it arrives AND an event occurs.
bag_data[bag_slot] = {
has_bag = false,
name = "",
max_slots = 0,
free_slots = 0,
used_slots = 0,
style = "",
color = "",
}
local slots = C_Container.GetContainerNumSlots(bag_slot)
-- Check type here to set slot style properly.
-- Profession bags are NOT included in overall free / used counts
local bag_type = "none"
is_prof_bag = IsProfessionBagID(bag_slot)
-- Blizz treats 'last' slot as a reagent only slot...
-- For our purpose, treat it as a profession bag.
if is_prof_bag or bag_slot == 5 then
bag_type = "profession"
else
bag_type = "normal"
end
bag_data[bag_slot].style = bag_type
if slots > 0 then
bag_data[bag_slot].has_bag = true
local bag_name = (C_Container.GetBagName(bag_slot) or UNKNOWN)
bag_data[bag_slot].name = bag_name
bag_data[bag_slot].max_slots = slots
local free = C_Container.GetContainerNumFreeSlots(bag_slot)
local used = slots - free
bag_data[bag_slot].free_slots = free
bag_data[bag_slot].used_slots = used
-- add to total
if bag_data[bag_slot].style == "profession" then
prof_slots = prof_slots + slots
prof_free = prof_free + free
prof_used = prof_used + used
else
total_slots = total_slots + slots
total_free = total_free + free
total_used = total_used + used
end
else
bag_data[bag_slot].has_bag = false
bag_data[bag_slot].name = NONE
end
if trace then
TitanPluginDebug(TITAN_BAG_ID, "...T GetBagData"
.. " " .. tostring(bag_slot) .. ""
.. " ?:" .. tostring(bag_data[bag_slot].has_bag) .. ""
.. " max: " .. tostring(bag_data[bag_slot].max_slots) .. ""
.. " used: " .. tostring(bag_data[bag_slot].used_slots) .. ""
.. " free: " .. tostring(bag_data[bag_slot].free_slots) .. ""
.. " type: " .. tostring(bag_data[bag_slot].style) .. ""
.. " prof: " .. tostring(is_prof_bag) .. ""
.. " '" .. tostring(bag_data[bag_slot].name) .. "'"
)
end
end
-- Normal bags
bag_data.total_slots = total_slots
bag_data.total_free = total_free
bag_data.total_used = total_used
-- Profession / reagent bags
bag_data.prof_slots = prof_slots
bag_data.prof_free = prof_free
bag_data.prof_used = prof_used
end
---plugin Handle registered events
---@param self Button
---@param event string
---@param ... any
local function OnEvent(self, event, ...)
if event == "PLAYER_ENTERING_WORLD" then
-- Leave in case future code is needed...
elseif event == "BAG_UPDATE" then
-- update the plugin text
TitanPanelButton_UpdateButton(TITAN_BAG_ID);
elseif event == "BAG_CONTAINER_UPDATE" then
-- 2024 Aug : Added as additional check if user swaps bags; may not be required
-- update the plugin text
TitanPanelButton_UpdateButton(TITAN_BAG_ID);
end
if trace then
TitanPluginDebug(TITAN_BAG_ID, "_OnEvent"
.. " " .. tostring(event) .. ""
)
end
end
---Opens all bags on a LeftClick
---@param self Button
---@param button string
local function OnClick(self, button)
if (button == "LeftButton") then
ToggleBags();
end
end
---Generate the plugin button text
---@param id string
---@return string
---@return string
local function GetButtonText(id)
GetBagData(id)
local bagText = ""
if TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots") then
bagText = format(L["TITAN_BAG_FORMAT"], bag_data.total_used, bag_data.total_slots);
else
bagText = format(L["TITAN_BAG_FORMAT"], bag_data.total_free, bag_data.total_slots);
end
local bagRichText = ""
if (TitanGetVar(TITAN_BAG_ID, "ShowColoredText")) then
local color = ""
color = TitanUtils_GetThresholdColor(TITAN_BAG_THRESHOLD_TABLE, bag_data.total_used / bag_data.total_slots);
bagRichText = TitanUtils_GetColoredText(bagText, color);
else
bagRichText = TitanUtils_GetHighlightText(bagText);
end
bagRichText = bagRichText
--..bagRichTextProf[1]..bagRichTextProf[2]..bagRichTextProf[3]..bagRichTextProf[4]..bagRichTextProf[5];
if trace then
TitanPluginDebug(TITAN_BAG_ID, "T GetBagData"
.. " '" .. tostring(bagRichText) .. "'"
)
end
return L["TITAN_BAG_BUTTON_LABEL"], bagRichText
end
---Determine the color based on percentage
---@param text string
---@param show_color boolean
---@param numerator number
---@param denom number
---@return string
local function ThresholdColor(text, show_color, numerator, denom)
local res = ""
local color = ""
if show_color then
if denom == 0 then
color = TitanUtils_GetThresholdColor(TITAN_BAG_THRESHOLD_TABLE, 1);
else
color = TitanUtils_GetThresholdColor(TITAN_BAG_THRESHOLD_TABLE, numerator / denom);
end
res = TitanUtils_GetColoredText(text, color);
else
-- use without color
res = TitanUtils_GetHighlightText(text);
end
return res
end
---Generate tooltip text
---@return string
local function GetTooltipText()
-- Normal shows free / used of total per user options.
-- Detailed shows list bags with profession bag counts in gray - not counted.
-- Hint shows if user selects open bags on left click.
--#region-- 2024 Aug (8.1.0) : With the addition of new 'reagent' slot, we dropped coloring & counting profession bags.
local returnstring = "";
local show_color = TitanGetVar(TITAN_BAG_ID, "ShowColoredText")
-- Collect names and x / y numbers, color numbers if user requested
if TitanGetVar(TITAN_BAG_ID, "ShowDetailedInfo") then
returnstring = "\n";
if TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots") then
returnstring = returnstring .. TitanUtils_GetNormalText(L["TITAN_BAG_MENU_TEXT"])
.. ":\t" .. TitanUtils_GetNormalText(L["TITAN_BAG_USED_SLOTS"]) .. ":\n";
else
returnstring = returnstring .. TitanUtils_GetNormalText(L["TITAN_BAG_MENU_TEXT"])
.. ":\t" .. TitanUtils_GetNormalText(L["TITAN_BAG_FREE_SLOTS"]) .. ":\n";
end
for bag = MIN_BAGS, MAX_BAGS do
local bagText = ""
local bagRichText
if bag_data[bag] then
if bag_data[bag].has_bag then
-- Format the x / y slots per user options
if (TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots")) then
bagText = format(L["TITAN_BAG_FORMAT"], bag_data[bag].used_slots, bag_data[bag].max_slots);
else
bagText = format(L["TITAN_BAG_FORMAT"], bag_data[bag].free_slots, bag_data[bag].max_slots);
end
-- Format x / y per user options
if bag_data[bag].style == "profession" then
bagRichText = TitanUtils_GetGrayText(bagText)
else
bagRichText = ThresholdColor(bagText, show_color, bag_data[bag].used_slots, bag_data[bag].max_slots)
end
else
bagRichText = ""
end
-- Format bag name as 'normal' 2024 Aug
local name_text = TitanUtils_GetNormalText(bag_data[bag].name)
returnstring = returnstring .. name_text .. "\t" .. bagRichText .. "\n";
else
--Silent error - should never get here...
end
end
returnstring = returnstring .. "------\t" .. "---\n";
end
-- Always show free / used of max slots to user
local xofy = ""
local slots = ""
if TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots") then
xofy = "" .. tostring(bag_data.total_used) .. "/" .. tostring(bag_data.total_slots)
xofy = ThresholdColor(xofy, show_color, bag_data.total_used, bag_data.total_slots)
slots = L["TITAN_BAG_USED_SLOTS"]
else
xofy = "" .. tostring(bag_data.total_free) .. "/" .. tostring(bag_data.total_slots)
xofy = ThresholdColor(xofy, show_color, bag_data.total_free, bag_data.total_slots)
slots = L["TITAN_BAG_FREE_SLOTS"]
end
returnstring = returnstring .. TitanUtils_GetNormalText(slots) .. ":\t" .. xofy .. "\n"
-- Add Hint if user wants to open bags on left click.
if TitanGetVar(TITAN_BAG_ID, "OpenBags") then
returnstring = returnstring .. "\n" .. TitanUtils_GetGreenText(L["TITAN_BAG_TOOLTIP_HINTS"])
else
-- nop
end
return returnstring
end
---Generate and display rightclick menu options for user.
local function PrepareBagMenu()
local info
-- level 1
TitanPanelRightClickMenu_AddTitle(TitanPlugins[TITAN_BAG_ID].menuText);
info = {};
info.text = L["TITAN_BAG_MENU_SHOW_USED_SLOTS"];
info.func = function()
TitanSetVar(TITAN_BAG_ID, "ShowUsedSlots", 1);
TitanPanelButton_UpdateButton(TITAN_BAG_ID);
end
info.checked = TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots");
TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());
info = {};
info.text = L["TITAN_BAG_MENU_SHOW_AVAILABLE_SLOTS"];
info.func = function()
TitanSetVar(TITAN_BAG_ID, "ShowUsedSlots", nil);
TitanPanelButton_UpdateButton(TITAN_BAG_ID);
end
info.checked = TitanUtils_Toggle(TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots"));
TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());
info = {};
info.text = L["TITAN_BAG_MENU_SHOW_DETAILED"];
info.func = function()
TitanToggleVar(TITAN_BAG_ID, "ShowDetailedInfo");
end
info.checked = TitanGetVar(TITAN_BAG_ID, "ShowDetailedInfo");
TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());
info = {};
info.text = L["TITAN_BAG_MENU_OPEN_BAGS"]
info.func = function()
TitanToggleVar(TITAN_BAG_ID, "OpenBags")
end
info.checked = TitanGetVar(TITAN_BAG_ID, "OpenBags");
TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());
TitanPanelRightClickMenu_AddSpacer();
TitanPanelRightClickMenu_AddControlVars(TITAN_BAG_ID)
end
---plugin Registers the plugin and simple init
---@param self Button
local function OnLoad(self)
local notes = ""
.. "Adds bag and free slot information to Titan Panel.\n"
.. "- Open bags should work... Retail taint fixed Apr 2024 (10.2.7).\n"
.. "- Professions counts moved to tooltip only : Aug 2024 (Titan 8.1.0).\n"
self.registry = {
id = TITAN_BAG_ID,
category = "Built-ins",
version = TITAN_VERSION,
menuText = L["TITAN_BAG_MENU_TEXT"],
menuTextFunction = PrepareBagMenu,
buttonTextFunction = GetButtonText,
tooltipTitle = L["TITAN_BAG_TOOLTIP"],
tooltipTextFunction = GetTooltipText,
icon = "Interface\\AddOns\\TitanBag\\TitanBag",
iconWidth = 16,
notes = notes,
controlVariables = {
ShowIcon = true,
ShowLabelText = true,
ShowColoredText = true,
DisplayOnRightSide = true,
},
savedVariables = {
ShowUsedSlots = 1,
ShowDetailedInfo = false,
ShowIcon = 1,
ShowLabelText = 1,
ShowColoredText = 1,
DisplayOnRightSide = false,
OpenBags = true,
}
};
-- As of Apr 2024 (10.2.7) the taint on opening bags in Retail is fixed.
-- Reagent bag slot added end of DragonFlight for War Within expansion.
if NUM_TOTAL_EQUIPPED_BAG_SLOTS == nil then -- NOT Retail as of DragonFlight WHY BLIZZ!?
MAX_BAGS = Constants.InventoryConstants.NumBagSlots
else -- Classic API
MAX_BAGS = NUM_TOTAL_EQUIPPED_BAG_SLOTS
end
self:RegisterEvent("PLAYER_ENTERING_WORLD");
end
---Prep and update plugin button here to minimize resources.
---@param self Button
local function OnShow(self)
-- Register for bag updates and update the plugin text
self:RegisterEvent("BAG_UPDATE")
self:RegisterEvent("BAG_CONTAINER_UPDATE")
TitanPanelButton_UpdateButton(TITAN_BAG_ID);
end
---Shutdown plugin button here to minimize resources.
---@param self Button
local function OnHide(self)
self:UnregisterEvent("BAG_UPDATE")
self:UnregisterEvent("BAG_CONTAINER_UPDATE")
end
---Create needed plugin frames
local function Create_Frames()
if _G[TITAN_BUTTON] then
return -- if already created
end
-- general container frame
local f = CreateFrame("Frame", nil, UIParent)
-- f:Hide()
-- Titan plugin button
local window = CreateFrame("Button", TITAN_BUTTON, f, "TitanPanelComboTemplate")
window:SetFrameStrata("FULLSCREEN")
-- Using SetScript("OnLoad", does not work
OnLoad(window);
-- TitanPanelButton_OnLoad(window); -- Titan XML template calls this...
window:SetScript("OnShow", function(self)
OnShow(self);
TitanPanelButton_OnShow(self);
end)
window:SetScript("OnHide", function(self)
OnHide(self)
end)
window:SetScript("OnEvent", function(self, event, ...)
OnEvent(self, event, ...)
end)
window:SetScript("OnClick", function(self, button)
OnClick(self, button);
TitanPanelButton_OnClick(self, button);
end)
end
Create_Frames() -- do the work
--[[
TitanDebug("T isP 0:"
.." "..tostring(slot)..""
.." "..tostring(itemId)..""
.." '"..tostring(itemType).."'"
.." '"..tostring(itemSubType).."'"
.." "..tostring(itemEquipLoc)..""
.." '"..tostring(itemTexture).."'"
.." "..tostring(classID)..""
.." "..tostring(subclassID)..""
)
--]]