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

1204 lines
39 KiB
Lua

-- Bagshui Core
-- Customizable auto-categorizing and sorting all-in-one inventory replacement for Vanilla WoW 1.12.
-- https://github.com/veechs/Bagshui
--
-- Most code is split into file in Components; what's here is close to the minimum to get things bootstrapped.
--
-- Raises:
-- - BAGSHUI_ADDON_LOADED
-- - BAGSHUI_LOG_UPDATE
-- Provide access to the global environment without using `getglobal()`.
-- Must be declared before `bagshuiEnvironment` because things in there use it.
local _G = _G or getfenv()
-- This environment will be loaded for all Bagshui code via `setfenv()`.
-- Top-level constants should be declared here as `BS_CONSTANT_NAME`.
-- Other constants can be added later via `Bagshui:AddConstants()`.
local bagshuiEnvironment = {
-- Pull basic information into the environment.
BS_VERSION = "", -- Intentionally empty; will be filled from Bagshui.toc by `Bagshui:AddonLoaded()`.
-- Increment to wipe SavedVariables.
BS_DATA_VERSION = 1,
-- Imports of this version or lower can be processed.
-- Stored in the `bagshuiExportFormat` key of the export object table.
-- This may never change, but it seems like a good thing to track just in case.
BS_EXPORT_VERSION = 1,
-- Bagshui homepage.
BS_URL = "github.com/veechs/Bagshui",
-- Bagshui wiki root. MUST end in a slash.
BS_WIKI_URL = "github.com/veechs/Bagshui/wiki/",
-- Bagshui wiki pages. Need to be concatenated onto `BS_WIKI_URL`.
BS_WIKI_PAGES = {
EditMode = "Edit-Mode",
Categories = "Categories",
CharacterData = "Searching#managing-character-data",
Profiles = "Profiles",
Rules = "Rules",
Search = "Searching",
Share = "Import-Export",
SortOrders = "Sort-Orders",
},
-- Fallback locale, used if no matching locale for the current WoW client is found.
BS_DEFAULT_LOCALE = "enUS",
-- Defaults for categorizing and sorting.
BS_DEFAULT_CATEGORY_ID = "Uncategorized",
BS_DEFAULT_CATEGORY_SEQUENCE = 30,
BS_DEFAULT_BUILTIN_CATEGORY_SEQUENCE = 90,
-- This has to be here, not in SortOrders.lua, so it's available to Setting configuration.
BS_DEFAULT_SORT_ORDER_ID = "Default",
-- Default profiles used at first login and for fallback if the configured default profile is unavailable.
-- ```
-- { [BS_PROFILE_TYPE value] = "ProfileId" }
-- ```
---@type table<BS_PROFILE_TYPE, string>
BS_DEFAULT_PROFILE = {
Structure = "Bagshui",
Design = "Bagshui",
},
---@enum BS_INVENTORY_TYPE
-- Allowed inventory class types.
-- Note that values get lowercased automatically for SavedVariables storage.
-- ```
-- { UPPERCASE = "PascalCase" }
-- ```
---@type table<string, string>
BS_INVENTORY_TYPE = {
BAGS = "Bags",
BANK = "Bank",
KEYRING = "Keyring",
},
-- Control the order in which toolbar icons and menu items for inventory types appear.
-- ```
-- { [BS_INVENTORY_TYPE value1], ... [BS_INVENTORY_TYPE valueN] }
-- ```
---@type string[]
BS_INVENTORY_TYPE_UI_ORDER = {
"Bags",
"Bank",
"Keyring",
},
---@enum BS_PROFILE_TYPE
-- Allowed profile types.
-- ```
-- { UPPERCASE = "PascalCase" }
-- ```
---@type table<string, string>
BS_PROFILE_TYPE = {
STRUCTURE = "Structure",
DESIGN = "Design",
},
-- Control the order in which the Settings menus for default profiles appear.
-- ```
-- { [BS_PROFILE_TYPE key1], ... [BS_PROFILE_TYPE key2] }
-- ```
---@type string[]
BS_PROFILE_ORDER = {
"STRUCTURE",
"DESIGN",
},
-- Names of keys in the config file for various types of data.
-- ```
-- { UPPERCASE = "camelCase" }
-- ```
---@type table<string, string>
BS_CONFIG_KEY = {
CHARACTERS = "characters",
CHARACTER_INFO = "info",
COLORS = "colors",
DATA_VERSION = "dataVersion",
OBJECT_VERSIONS = "objectVersions",
LOG = "log",
},
---@enum BS_SETTING_APPLICABILITY
-- Used in Config\Settings.lua and elsewhere to specify what object a group of settings apply to.
-- ```
-- { UPPERCASE = "UPPERCASE" }
-- ```
---@type table<string, string>
BS_SETTING_APPLICABILITY = {
INVENTORY = "INVENTORY",
GROUP = "GROUP",
},
---@enum BS_SETTING_SCOPE
-- Used in Config\Settings.lua to specify the `scope` of a setting (i.e. what level it's stored at).
-- See `Settings:InitSettingsInfo()` for more information.
-- ```
-- { UPPERCASE = "UPPERCASE" }
-- ```
---@type table<string, string>
BS_SETTING_SCOPE = {
ACCOUNT = "ACCOUNT",
CHARACTER = "CHARACTER",
INVENTORY = "INVENTORY",
},
---@enum BS_SETTING_PROFILE_SCOPE
-- Used in Config\Settings.lua to specify the `profileScope` of a setting. Only works when
-- `scope` is "INVENTORY". See `Settings:InitSettingsInfo()` for more information.
-- ```
-- { UPPERCASE = "UPPERCASE" }
-- ```
---@type table<string, string>
BS_SETTING_PROFILE_SCOPE = {
STRUCTURE = "STRUCTURE",
DESIGN = "DESIGN",
BEHAVIOR = "BEHAVIOR",
},
-- Data types for settings, used in a setting's `type` property in Config\Settings.lua.
-- The Settings class expects the values of these entries to correspond to `Settings:Validate<value>()`.
-- For example, `NUMBER = "Number"` would map to `Settings:ValidateNumber()`.
-- ```
-- { UPPERCASE = "PascalCase" }
-- ```
---@type table<string, string>
BS_SETTING_TYPE = {
BOOL = "Bool",
BOOLEAN = "Bool",
CHOICES = "Choices",
COLOR = "ColorRgba",
NUMBER = "Number",
TRIGGER = "Trigger",
PLACEHOLDER = "Placeholder",
},
---@enum BS_AUTO_SPLIT_MENU_TYPE
-- Globally recognized auto-split menu types.
-- Needs to be defined here so it can be used in Config\Settings.lua.
-- ```
-- { UPPERCASE = "PascalCase" }
-- ```
---@type table<string, string>
BS_AUTO_SPLIT_MENU_TYPE = {
CATEGORIES = "Categories",
CHARACTERS = "Characters",
PROFILES = "Profiles",
SORT_ORDERS = "SortOrders",
SORT_FIELDS = "SortFields",
},
----@alias BS_AUTO_SPLIT_MENU_TYPE table<string, string>
-- Custom colors.
-- ```
-- { UPPERCASE = { r, g, b, [a] } }
-- ```
---@type table<string, number[]>
BS_COLOR = {
-- This is slightly different from BS_FONT_COLOR.BAGSHUI
-- because colors don't look quite the same when applied to
-- large areas.
BAGSHUI_LOGO = { 0.692, 0.139, 0.416 },
-- General colors.
RED = { 0.8, 0, 0 },
FULL_RED = { 1, 0, 0 },
DARK_RED = { 1, 0.1, 0.1 },
GRAY = { 0.5, 0.5, 0.5 },
GREEN = { 0, 0.8, 0 },
LIGHT_GREEN = { 0.2, 0.9, 0.2 },
DARK_GREEN = { 1, 0.8, 0.1 },
WHITE = { 1, 1, 1 },
YELLOW = { 0.9, 0.72, 0.15 }, -- Default toolbar button color.
-- Factions.
FACTION_ALLIANCE = { 0.15, 0.25, 0.95 },
FACTION_HORDE = { 0.85, 0.15, 0.10 },
FACTION_UNKNOWN = { 0.60, 0.50, 0.30 },
-- Item slot button texture colors.
ITEM_SLOT_STATE_NORMAL = { 1, 1, 1, 1 },
ITEM_SLOT_STATE_LOCKED = { 0.5, 0.5, 0.5, 1 },
ITEM_SLOT_HIGHLIGHT = { 1, 1, 0.35 },
-- Icon button colors.
UI_GREEN = { 0.6, 0.82, 0.3 },
UI_ORANGE = { 1, 0.45, 0.1 },
ICON_BUTTON_HOVER = { 0.95, 0.9, 0.9, 0.25 },
CODE_EXAMPLE = { 0.83, 0.47, 0.65 },
-- Print() and friends colors.
PRINT_BAGSHUI = { 0.86, 0.259, 0.573 },
PRINT_ERROR = { 0.95, 0, 0 },
PRINT_WARNING = { 0.95, 0.45, 0 },
},
-- Custom font colors, defined as "|cff<hex color>".
-- Everything from `BS_COLOR` will automatically be converted to escape strings
-- populated automatically by `Bagshui:PostUtilInit()`.
---@type table<string, string>
BS_FONT_COLOR = {
BAGSHUI = "|cffdb4e97",
},
-- Predefined border types, consumed by `Ui:SetFrameBackdrop()`.
-- ```
-- { UPPERCASE = { `<frame>:SetBackdrop()` table } }
-- ```
---@type table<string, table>
BS_BORDER = {
NONE = {},
CURVED = {
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 16,
insets = 4,
},
SOLID = {
edgeFile = "Interface\\Buttons\\WHITE8X8",
edgeSize = 1,
insets = 1,
},
},
-- Tooltip hover delays, used by `Bagshui:ShowTooltipAfterDelay()` and associated functions.
-- ```
-- { UPPERCASE = <number> }
-- ```
---@type table<string, number>
BS_TOOLTIP_DELAY_SECONDS = {
DEFAULT = 1,
TOOLBAR_DEFAULT = 2,
TOOLBAR_EXPAND_DEFAULT = 4,
-- After a tooltip is shown, how long should the SHORTENED value be used?
USE_SHORTENED_AFTER_LAST_SHOW_FOR = 2,
SHORTENED = 0.3,
},
-- It looks nicer to indent some menu values under headers (like the Edit Mode menu for groups).
---@type string
BS_MENU_SUBTITLE_INDENT = " ",
-- When checking whether a window with a saved position might be offscreen,
-- how close to the edge does it need to be?
BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD = 50,
-- In Edit Mode, rows may be at least this many columns wide (keeps the window from getting very tall).
BS_EDIT_MODE_MIN_MAX_COLUMNS = 20,
-- Empty item used to initialize inventory cache entries.
-- Values here can't be nil or Lua drops them.
-- ```
-- { camelCase = <value> }
-- ```
---@type table<string, string|number|table>
BS_ITEM_SKELETON = {
baseName = "", -- Name without random suffix, if applicable.
bagNum = -99, -- Using an invalid container ID since 0 is Backpack.
bagType = "",
charges = 0,
count = 0,
equipLocation = "",
equipLocationLocalized = "",
equipLocationSort = 0,
emptySlot = 0, -- Using 0/1 instead of true/false for easy sorting.
id = 0,
itemLink = "", -- Item link (|cffffffff|Hitem:12345:0:0:0|h[Name]|h|r).
itemString = "", -- Item string (item:12345:0:0:0).
locked = 0,
lockPickable = 0,
maxStackCount = 0,
minLevel = "",
name = "",
openable = 0,
quality = 1,
qualityLocalized = "",
readable = 0,
slotNum = 0,
subtype = "",
suffixName = "", -- Random suffix, if applicable.
tooltip = "",
texture = "Interface\\Icons\\INV_Misc_QuestionMark",
type = "",
uncategorized = 0, -- Using 0/1 instead of true/false for easy sorting.
bagshuiGroupId = "",
bagshuiCategoryId = "",
bagshuiDate = -1,
bagshuiInventoryType = "",
bagshuiStockState = "",
-- Disabled properties and reasons for disabling.
-- itemStringGeneric is itemString without enchantId and uniqueId. This
-- was never used anywhere in EngInventory, so there doesn't seem to be
-- any real use for it. It's commented out throughout Bagshui in case
-- there's ever a reason to bring it back.
-- itemStringGeneric = "",
},
-- Which properties can be used in sort orders?
-- ```
-- { [BS_ITEM_SKELETON key1], ... [BS_ITEM_SKELETON keyN] }
-- ```
---@type string[]
BS_ITEM_PROPERTIES_ALLOWED_IN_SORT_ORDERS = {
"bagNum",
"bagType",
"charges",
"count",
"equipLocationSort",
"emptySlot",
"id",
"itemLink",
"itemString",
"minLevel",
"name",
"quality",
"slotNum",
"subtype",
"tooltip",
"type",
"uncategorized",
},
-- ItemInfo:InitializeItem() will never reset these properties.
---@type table<string, boolean>
BS_ITEM_PROTECTED_PROPERTIES = {
bagNum = true,
bagType = true,
slotNum = true,
},
-- ItemInfo:InitializeItem() will not set these properties to default if nil.
-- Typically needed for properties that are used for empty slot identification.
---@type table<string, boolean>
BS_ITEM_NIL_PROPERTIES = {
itemLink = true,
itemString = true,
texture = true, -- Without this, empty slots for offline inventories will become ? icons.
},
-- Rule functions that correspond to item properties.
-- This controls what appears in the Item Information menu, tooltip, and window.
-- ```
-- { [BS_ITEM_SKELETON key] = { "Function1()", "FunctionN(%s)" }|true }
-- ```
-- * `%s` will be replaced with the item property value.
-- * Set `true` if there is no rule function but the property should still appear.
---@type table<string, string[]|boolean>
BS_ITEM_PROPERTIES_TO_FUNCTIONS = {
activeQuest = { "ActiveQuest()" },
bagNum = { "Bag(%s)" },
bagType = { 'BagType("%s")' },
bindsOnEquip = { "BindsOnEquip()" },
count = { "Count(%s)", "Count(Min, Max)" },
emptySlot = { "EmptySlot()" },
equipLocation = { 'EquipLocation("%s")' },
equipLocationLocalized = { 'EquipLocation("%s")' },
id = { "Id(%s)" },
itemString = { 'ItemString("%s")' },
minLevel = { "MinLevel(%s)", "CharacterLevelRange(±levels)", "CharacterLevelRange(LevelsBelow, LevelsAbove)" },
name = { 'Name("%s")', 'NameExact("%s")' },
periodicTable = { 'PeriodicTable("%s")' },
quality = { "Quality(%s)" },
qualityLocalized = { 'Quality("%s")' },
soulbound = { "Soulbound()" },
stacks = { "Stacks()" },
subtype = { 'Subtype("%s")' },
tooltip = { 'Tooltip("Search String")' },
type = { 'Type("%s")' },
uncategorized = true,
},
-- These will supplement and/or override the item's properties in the
-- Item Information menu/tooltip/window.
--
-- All references must be explicit here since the Bagshui environment isn't built up yet.
-- In other words, use `Bagshui.environment.L` instead of `L`, `Bagshui.components.Rules` instead of `BsRules`, etc.
--
-- ```
-- { [BS_ITEM_SKELETON key] = function(item, BS_ITEM_INFO_DISPLAY_TYPE) -> string|string[] }
-- ```
---@type table<string, function>
BS_REALTIME_ITEM_INFO_PROPERTIES = {
activeQuest = function(item)
return tostring(Bagshui.components.Rules:Match("ActiveQuest()", item))
end,
bindsOnEquip = function(item)
return tostring(Bagshui.components.Rules:Match("BindsOnEquip()", item))
end,
emptySlot = function(item)
return tostring(item.emptySlot == 1)
end,
periodicTable = function(item, displayType)
Bagshui.components.ItemInfo:GetPeriodicTableSets(item)
-- Only show the count of sets in the right-click menu or tooltip.
if displayType ~= Bagshui.environment.BS_ITEM_INFO_DISPLAY_TYPE.TEXT then
return string.format(
Bagshui.environment.L.Suffix_Sets,
table.getn(Bagshui.components.ItemInfo.periodicTableSetCache)
)
end
-- Text mode (Item Information window) should show the entire list of sets.
return Bagshui.components.ItemInfo.periodicTableSetCache
end,
soulbound = function(item)
return tostring(Bagshui.components.Rules:Match("Soulbound()", item))
end,
stacks = function(item)
return tostring(Bagshui.components.Rules:Match("Stacks()", item))
end,
tooltip = function(item, displayType)
if item.tooltip == "" then
return nil
end
-- Text mode (Item Information window) should show the entire tooltip.
if displayType == Bagshui.environment.BS_ITEM_INFO_DISPLAY_TYPE.TEXT then
return "\n" .. item.tooltip
end
-- Only show the first line of the tooltip in the right-click menu or tooltip.
-- (%C is non-control characters, which excludes newline.)
local _, _, tooltip = string.find(item.tooltip, "^(%C+)")
if tooltip ~= item.tooltip then
tooltip = string.format(Bagshui.environment.L.Symbol_Ellipsis, tooltip)
end
return tooltip
end,
uncategorized = function(item)
return tostring(item.uncategorized == 1)
end,
},
-- Item properties that will be hidden from the Bagshui info tooltip when the item isn't in a bag.
---@type table<string, true>
BS_ITEM_PROPERTIES_SUPPRESSED_IN_TOOLTIP_OUTSIDE_INVENTORY = {
bagNum = true,
count = true, -- Count is always 0 and looks silly.
emptySlot = true,
periodicTable = true, -- Could allow this in the future if a way to open the info window for items not in inventory is devised.
tooltip = true, -- Same as periodicTable.
slotNum = true,
uncategorized = true,
},
-- List of item properties sorted by friendly (localized) name.
-- Automatically built out by `ItemInfo:BuildSortedItemTemplatePropertyList()` the first time it's needed.
---@type string[]
BS_ITEM_PROPERTIES_SORTED = nil,
-- Used to get numbers for slot names so they can be sorted.
---@type table<string, number>
BS_INVENTORY_EQUIP_LOCATION_SORT_ORDER = {
INVTYPE_HEAD = 1,
INVTYPE_NECK = 2,
INVTYPE_SHOULDER = 3,
INVTYPE_CLOAK = 4, -- Back
INVTYPE_CHEST = 5,
INVTYPE_ROBE = 5, -- Chest
INVTYPE_BODY = 6, -- Shirt
INVTYPE_TABARD = 7,
INVTYPE_WRIST = 8,
INVTYPE_HAND = 9,
INVTYPE_WAIST = 10,
INVTYPE_LEGS = 11,
INVTYPE_FEET = 12,
INVTYPE_FINGER = 13,
INVTYPE_FINGER_OTHER = 14,
INVTYPE_TRINKET = 15,
INVTYPE_TRINKET_OTHER = 16,
INVTYPE_2HWEAPON = 17, -- Main Hand
INVTYPE_WEAPON = 17, -- Main Hand
INVTYPE_WEAPONMAINHAND = 17, -- Main Hand
INVTYPE_HOLDABLE = 18, -- Offhand
INVTYPE_SHIELD = 18, -- Offhand
INVTYPE_WEAPON_OTHER = 18, -- Offhand
INVTYPE_WEAPONOFFHAND = 18, -- Offhand
INVTYPE_RANGED = 19,
INVTYPE_RELIC = 19, -- Ranged
INVTYPE_WAND = 19, -- Ranged
INVTYPE_GUN = 19, -- Ranged
INVTYPE_CROSSBOW = 19, -- Ranged
INVTYPE_THROWN = 19, -- Ranged
INVTYPE_PROJECTILE = 20, -- Ammo
},
-- In addition to the bagshuiStockState item property, stock state table values are used
-- to determine the following stock badge attributes, so they must be PascalCase:
-- - Textures: `ItemSlot\Stock-<value>.tga`
-- - Colors: `settings.itemStockBadge<value>Color`
-- - Tooltip strings: `L["Stock_" .. <value>]`
-- ```
-- { UPPERCASE = "PascalCase" }
-- ```
---@type table<string, string>
BS_ITEM_STOCK_STATE = {
NEW = "New",
UP = "Up",
DOWN = "Down",
NO_CHANGE = "",
},
---@enum BS_INVENTORY_BAG_USAGE_DISPLAY
-- Control when bag usage is displayed.
-- Values must match Setting_BagUsageDisplay_* localization entries.
---@type table<string, string>
BS_INVENTORY_BAG_USAGE_DISPLAY = {
ALWAYS = "Always",
SMART = "Smart",
NEVER = "Never",
},
---@enum BS_INVENTORY_BAG_USAGE_FORMAT
-- Control how bag usage is displayed.
-- Values must match Setting_BagUsageFormat_* localization entries.
---@type table<string, string>
BS_INVENTORY_BAG_USAGE_FORMAT = {
EMPTY = "Empty",
EMPTY_TOTAL = "Empty_Total",
EMPTY_USED_TOTAL = "Empty_Used_Total",
USED = "Used",
USED_TOTAL = "Used_Total",
USED_EMPTY_TOTAL = "Used_Empty_Total",
},
---@enum BS_GAME_PLAYER_GROUP_TYPE
-- Used by GameInfo for the `playerGroupType` property.
---@type table<string, string>
BS_GAME_PLAYER_GROUP_TYPE = {
PARTY = "Party",
RAID = "Raid",
SOLO = "Solo",
},
---@enum BS_RULE_ENVIRONMENT_VARIABLES
-- Variables that are pre-populated into the Rule evaluation environment.
-- Rule functions can add their own as well.
---@type table<string, string>
BS_RULE_ENVIRONMENT_VARIABLES = {
Active = "~Active~",
Eligible = "~Eligible~",
Inactive = "~Inactive~",
Ineligible = "~Ineligible~",
},
---@enum BS_RULE_ARGUMENT_TYPE
-- Define what argument types are accepted by the `validArgumentTypes` parameter of `Rules:TestItemAttribute()`.
-- - Keys are one or more Lua types **in UPPERCASE** separated by commas with **no spaces**.
-- - Every possible order of key types must be included, so if `STRING,NUMBER` is a key, `NUMBER,STRING` must also be a key.
-- (This could be automated but it's not really worth it.)
-- - Values are tables with keys as Lua types and values of true.
-- ```
-- { ["TYPE1,TYPE2"] = { type1 = true, typeN = true} }
-- ```
---@type table<string, table<string, boolean>>
BS_RULE_ARGUMENT_TYPE = {
["STRING"] = {
string = true,
},
["NUMBER"] = {
number = true,
},
["STRING,NUMBER"] = {
string = true,
number = true,
},
["NUMBER,STRING"] = {
string = true,
number = true,
},
},
---@enum BS_RULE_MATCH_TYPE
-- What types of matches will `Rules:TestItemAttribute()` accept?
-- ```
-- { UPPERCASE = "UPPERCASE" }
-- ```
---@type table<string, string>
BS_RULE_MATCH_TYPE = {
EQUALS = "EQUALS",
CONTAINS = "CONTAINS",
BETWEEN = "BETWEEN",
},
---@enum BS_HOOK_ACTION
-- Hook class action parameter values.
-- ```
-- { UPPERCASE = "PascalCase" }
-- ```
---@type table<string, string>
BS_HOOK_ACTION = {
REGISTER = "Register",
UNREGISTER = "Unregister",
CHECK = "Check",
},
-- Output indentation.
BS_INDENT = " ",
---@enum BS_LOG_MESSAGE_TYPE
-- Log message severities.
-- ```
-- { UPPERCASE = number }
-- ```
---@type table<string, number>
BS_LOG_MESSAGE_TYPE = {
INFORMATION = 1,
WARNING = 2,
ERROR = 3,
},
-- How to localize log message severities.
-- - Array index is severity number from `BS_LOG_MESSAGE_TYPE`,
-- - Value is the string to look up in `L`.
---@type string[]
BS_LOG_TYPE_LOCALIZATION_KEY = {
"Log_Info",
"Log_Warn",
"Log_Error",
},
-- How to color log messages based on severity.
-- - Array index is severity number from `BS_LOG_MESSAGE_TYPE`,
-- - Value is the font formatting string.
---@type string[]
BS_LOG_MESSAGE_COLOR = {
_G.HIGHLIGHT_FONT_COLOR_CODE,
"|cFFFF5F00",
_G.RED_FONT_COLOR_CODE,
},
-- Keep this many log entries.
---@type number
BS_LOG_LIMIT = 75,
-- Date formatting for log entries.
---@type string
BS_LOG_DATE_STRING = "%Y-%m-%d %H:%M:%S",
-- Ensure debug is never turned on in production by requiring another addon to set BAGSHUI_DEBUG_ON to true prior to Bagshui loading.
-- The !BagshuiDebug addon (in the Debug folder) is an easy way to enable this.
---@type boolean
BS_DEBUG = _G.BAGSHUI_DEBUG_ON or false,
-- Is SuperWoW loaded? Since it's a DLL injection, not an addon, IsAddOnLoaded() won't help here.
---@type boolean
BS_SUPER_WOW_LOADED = type(_G.SetAutoloot) == "function",
-- Localization helpers.
BS_NEWLINE = "\n",
-- These are accessible via metatable, but explicit access is going to be slightly faster.
-- Not capturing font colors in case they're changed.
_G = _G,
error = _G.error,
ipairs = _G.ipairs,
pairs = _G.pairs,
loadstring = _G.loadstring,
math = _G.math,
mod = _G.mod,
tonumber = _G.tonumber,
tostring = _G.tostring,
string = _G.string,
table = _G.table,
type = _G.type,
-- Other addons.
-- Intensive check for pfUI to ensure it's the real thing and not an addon that
-- provides pfUI API compatibility. (And false is used since nil will just
-- end up with an empty table entry and it will metatable its way up into _G).
pfUI = (_G.IsAddOnLoaded("pfUI") and _G.pfUI and _G.pfUI.uf and _G.pfUI.env and _G.pfUI.env.C) and _G.pfUI or false,
-- Contain the _ dummy variable within the Bagshui environment.
_ = 0,
}
-- Configure environment so global variables are still accessible.
setmetatable(bagshuiEnvironment, { __index = _G })
-- Replace current execution environment with Bagshui environment.
---@diagnostic disable-next-line: param-type-mismatch
setfenv(1, bagshuiEnvironment)
-- Namespace for everything Bagshui
local Bagshui = {
-- Class instances go here.
-- ```
-- { PascalCase = class }
-- ```
---@type table<string, table>
components = {},
-- Class prototypes go here.
-- ```
-- { PascalCase = prototype }
-- ```
---@type table<string, table>
prototypes = {},
-- Configuration files in the Config folder should add data here.
---@type table<string, any>
config = {},
-- Embedded libraries.
---@type table<string, table>
libs = {
-- PeriodicTableEmbed is added when Libs\PeriodicTable.xml is processed, prior to Bagshui.lua.
PT = _G.PeriodicTableEmbed:GetInstance("1"),
},
-- Name and realm identifier of the current character.
-- Populated in `Bagshui:AddonLoaded()`.
---@type string
currentCharacterId = nil,
-- Pointer to SavedVariables data for the current character.
---@type table
currentCharacterData = {},
-- Relationship between docked inventory class instances.
-- For example, if Keyring is docked to Bags, `{ "Bags" = "Keyring" }`.
-- This only allows for one docking per class, but that's fine for now.
---@type table<BS_INVENTORY_TYPE, BS_INVENTORY_TYPE>
dockedInventories = {},
-- List of active quest items by name.
-- See Components\ActiveQuestItems.lua for details.
---@type table<string, { questName: string, needed: number, obtained: number }>
activeQuestItems = nil,
-- WoW API functions that need to be hooked at the Bagshui level.
-- ```
-- { ApiFunctionName = "BagshuiClassFunctionName" }
-- ```
---@type table<string, string>
apiFunctionsToHook = {
ClearCursor = "ClearCursor",
CloseAllWindows = "CloseAllWindows",
DeleteCursorItem = "ClearCursor",
MoneyFrame_UpdateMoney = "MoneyFrame_UpdateMoney",
OpenStackSplitFrame = "OpenStackSplitFrame",
PickupBagFromSlot = "PickupInventoryItem",
PickupInventoryItem = "PickupInventoryItem",
PutItemInBag = "PickupInventoryItem",
ToggleDropDownMenu = "ToggleDropDownMenu",
UIDropDownMenu_AddButton = "UIDropDownMenu_AddButton",
},
-- Bagshui's hidden frame for event handling, declared in `Bagshui:Init()`.
---@type table
eventFrame = nil,
-- List of components and events for which to call the OnEvent function.
-- After calling `Bagshui:RegisterEvent()`, the table will start looking something like this:
-- ```
-- {
-- [classObject Pointer] = {
-- _eventFunctionName = function,
-- event1 = true,
-- event2 = true
-- }
-- }
-- ```
---@type table<string, table>
eventConsumers = {},
-- Reusable tables for the Bagshui event queue.
-- Keys for each sub-table will be the unique event identifier.
-- See `Bagshui:QueueEvent()`, `Bagshui:ProcessEventQueue()`, and `Bagshui:RaiseEvent()`.
---@type table<string, table>
queuedEvents = {
-- Events that are waiting to be raised and the time at which to raise them.
---@type table<string, number>
events = {},
-- First parameter for the callback function.
---@type table<string, any>
arg1 = {},
-- Second parameter for the callback function.
---@type table<string, any>
arg2 = {},
-- Third parameter for the callback function.
---@type table<string, any>
arg3 = {},
-- Fourth parameter for the callback function.
---@type table<string, any>
arg4 = {},
-- Class function callback tables, populated by Bagshui:QueueClassCallback()
-- and consumed by Bagshui:RaiseEvent().
-- Class references.
---@type table<string, table>
class = {},
-- Class function references.
---@type table<string, function>
classFunction = {},
},
-- Window Management.
-- Frames that should be closed when `CloseAllWindows()` fires.
---@type table<table, boolean>
framesToCloseOnEscape = {},
-- "Child Frames" prevent Esc from closing Inventory frames when visible.
---@type table<table, boolean>
childWindowFrames = {},
-- Tooltips can be shared across all Bagshui code.
---@type table<string, table>
tooltips = {},
-- Ui class instance, basically just used for access to the Bagshui Info tooltip (instantiated during `Init()`).
ui = nil,
-- List of log messages.
---@type string[]
log = nil, -- Initialized in `Bagshui:AddonLoaded()`.
-- Expose the Bagshui environment table so things can be added to it.
environment = bagshuiEnvironment,
-- Debugging.
debug = BS_DEBUG,
debugWipeConfigOnLoad = false and BS_DEBUG,
}
-- Expose Bagshui globally.
_G.Bagshui = Bagshui
-- Add Bagshui to environment for quick access by other components.
Bagshui.environment.Bagshui = Bagshui
Bagshui.environment.Bs = Bagshui
--- Basic initialization. Called when BAGSHUI_CORE_EVENT_FUNCTIONS_LOADED is raised.
function Bagshui:Init()
-- self:PrintDebug("Bagshui:Init()")
-- Hidden frame for event handling (see `Bagshui:OnEvent()`).
self.eventFrame = _G.CreateFrame("Frame")
self.eventFrame.bagshuiData = {
registeredEvents = {},
}
self.eventFrame:SetScript("OnEvent", function()
-- Vanilla WoW "passes" event parameters via global variables.
self:OnEvent(_G.event, _G.arg1, _G.arg2, _G.arg3, _G.arg4)
end)
self.eventFrame:SetScript("OnUpdate", function()
-- Check the event queue on every frame.
self:ProcessEventQueue()
end)
-- Enable for event spelunking only.
-- self.eventFrame:RegisterAllEvents()
-- Bagshui itself needs these events. Other classes register their own events.
self:RegisterEvent("ADDON_LOADED")
self:RegisterEvent("PLAYER_LOGIN")
self:RegisterEvent("BAGSHUI_UTIL_LOADED")
self:RegisterEvent("BAGSHUI_LOCALIZATION_LOADED")
self:RegisterEvent("PLAYER_LOGOUT")
-- Shared menu hosting frame.
self.menuFrame = _G.CreateFrame("Frame", "BagshuiMenuHostFrame", nil, "UIDropDownMenuTemplate")
self.menuFrame.bagshuiData = {
name = self.menuFrame:GetName(),
-- Populated by `Menus:ShowMenu()` and consumed by `Bagshui:IsMenuOpen()`.
lastMenuTypeLoaded = nil,
-- Can be set to true before calling `Menus:ShowMenu()` to avoid changing
-- level 1 anchors unnecessarily in `Bagshui:ToggleDropDownMenu()`.
noFirstLevelRepositionNeeded = nil,
}
-- Key binding header; doesn't need localization.
-- Localized versions of inventory key binding names are populated during Inventory class creation.
_G.BINDING_HEADER_Bagshui = "Bagshui"
-- Create hex font strings for all RGB colors.
if not self.environment.BS_FONT_COLOR then
self.environment.BS_FONT_COLOR = {}
end
for name, rgb in pairs(BS_COLOR) do
if not self.environment.BS_FONT_COLOR[name] then
local hex = BsUtil.RGBPercentToHex(rgb.r or rgb[1], rgb.g or rgb[2], rgb.b or rgb[3])
-- The ff prefix would be alpha value, but WoW ignores it and it just always needs to be ff.
self.environment.BS_FONT_COLOR[name] = "|cff" .. hex
end
end
-- Need to adjust this by the scale so it behaves as expected.
BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD = BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD / _G.UIParent:GetScale()
end
--- Called from `Bagshui:OnEvent()` when ADDON_LOADED is raised. This indicates SavedVariables are safe to use.
--- Bagshui components are loaded immediately after this.
function Bagshui:AddonLoaded()
-- self:PrintDebug("Bagshui:AddonLoaded()")
-- Grab version number from TOC file.
self.environment.BS_VERSION = tostring(_G.GetAddOnMetadata("Bagshui", "Version"))
-- Prepare game function hooks.
self.hooks = Bagshui.prototypes.Hooks:New(self.apiFunctionsToHook, self)
local dataVersionKey = BS_CONFIG_KEY.DATA_VERSION
local charactersKey = BS_CONFIG_KEY.CHARACTERS
-- "[Name] - [Realm]"
self.currentCharacterId = _G.UnitName("player") .. " - " .. BsUtil.Trim(_G.GetCVar("realmName"))
self.currentRealm = BsUtil.Trim(_G.GetCVar("realmName"))
-- Base config (if it doesn't exist, version doesn't match, or debug wipe is on).
if
_G.BagshuiData == nil
or (_G.BagshuiData[dataVersionKey] == nil)
or (_G.BagshuiData[dataVersionKey] ~= BS_DATA_VERSION)
or self.debugWipeConfigOnLoad
then
-- Display chat message on version mismatch only.
if
_G.BagshuiData ~= nil
and _G.BagshuiData[dataVersionKey] ~= nil
and _G.BagshuiData[dataVersionKey] ~= BS_DATA_VERSION
then
self:Print(string.format(L.BagshuiDataReset, tostring(_G.BagshuiData[dataVersionKey]), BS_DATA_VERSION))
end
_G.BagshuiData = {
[dataVersionKey] = BS_DATA_VERSION,
[charactersKey] = {},
}
end
-- Creating the object versions key here because it was added after 1.0.
if not _G.BagshuiData[BS_CONFIG_KEY.OBJECT_VERSIONS] then
_G.BagshuiData[BS_CONFIG_KEY.OBJECT_VERSIONS] = {}
end
self.objectVersions = _G.BagshuiData[BS_CONFIG_KEY.OBJECT_VERSIONS]
-- Color history (account-wide).
if _G.BagshuiData[BS_CONFIG_KEY.COLORS] == nil then
_G.BagshuiData[BS_CONFIG_KEY.COLORS] = {}
end
self.colorHistory = _G.BagshuiData[BS_CONFIG_KEY.COLORS]
-- Character data storage.
if _G.BagshuiData[charactersKey] == nil then
_G.BagshuiData[charactersKey] = {}
end
if _G.BagshuiData[charactersKey][self.currentCharacterId] == nil then
_G.BagshuiData[charactersKey][self.currentCharacterId] = {}
end
self.characters = _G.BagshuiData[charactersKey]
self.currentCharacterData = _G.BagshuiData[charactersKey][self.currentCharacterId]
-- Characters from other WoW accounts on this computer (read-only).
-- Filled by `Catalog:LoadSharedCharacters()` from files in Nampower's shared CustomData folder.
-- Kept separate from `self.characters` so they are never written to this account's SavedVariables.
self.sharedCharacters = {}
-- Character info (data will be populated by Character class).
if self.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO] == nil then
self.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO] = {}
end
self.currentCharacterInfo = self.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO]
-- Inventory validation.
-- This will ensure there are no errors when changes are made to the expected inventory cache structure.
for character, characterData in pairs(self.characters) do
for _, inventoryType in pairs(BS_INVENTORY_TYPE) do
inventoryType = string.lower(inventoryType)
if characterData[inventoryType] and characterData[inventoryType].inventory then
for containerNum, containerContents in pairs(characterData[inventoryType].inventory) do
for slotNum, item in ipairs(containerContents) do
BsItemInfo:InitializeItem(item, false, true)
end
end
end
end
end
-- Log storage.
if _G.BagshuiData[BS_CONFIG_KEY.LOG] == nil then
_G.BagshuiData[BS_CONFIG_KEY.LOG] = {}
end
self.log = _G.BagshuiData[BS_CONFIG_KEY.LOG]
end
--- Initialization that can't occur until after Components\Localization.lua has loaded.
--- Called from `Bagshui:OnEvent()` when BAGSHUI_LOCALIZATION_LOADED is raised.
function Bagshui:PostLocalizationInit()
-- Add "About" slash command.
BsSlash:AddHandler("About", function(tokens)
if not tokens[2] or BsUtil.MatchLocalizedOrNon(tokens[2], "dialog") then
BsAboutDialog:Open()
elseif BsUtil.MatchLocalizedOrNon(tokens[2], "text") then
Bagshui:PrintBare("Bagshui " .. BS_VERSION)
elseif BsUtil.MatchLocalizedOrNon(tokens[2], "help") then
BsSlash:PrintHandlers({ L.Dialog, L.Text }, "About")
end
end)
-- Add "Settings" slash command.
BsSlash:AddHandler("Settings", function(tokens)
for _, inventoryType in ipairs(BS_INVENTORY_TYPE_UI_ORDER) do
if BsUtil.MatchLocalizedOrNon(tokens[2], inventoryType) then
self.components[inventoryType]:Open()
self.components[inventoryType].menus:OpenMenu("Settings")
return
end
end
if not self._settingsHandler_LocalizedInventoryTypes then
self._settingsHandler_LocalizedInventoryTypes = {}
for _, inventoryType in ipairs(BS_INVENTORY_TYPE_UI_ORDER) do
table.insert(self._settingsHandler_LocalizedInventoryTypes, L[inventoryType])
end
end
BsSlash:PrintHandlers(self._settingsHandler_LocalizedInventoryTypes, "Settings")
end)
end
--- Primary event handler called by Bagshui.eventFrame's OnEvent script.
---@param event string Event name.
---@param arg1 any? First event argument, if any.
---@param arg2 any? Second argument, if any.
---@param arg3 any? Third argument, if any.
---@param arg4 any? Fourth argument, if any.
function Bagshui:OnEvent(event, arg1, arg2, arg3, arg4)
-- Bagshui:PrintDebug("Bagshui event " .. event .. " // " .. tostring(arg1) .. " // " .. tostring(arg2) .. " // " .. tostring(arg3) .. " // " .. tostring(arg4))
local downstreamEvent = event --[[@as string|nil]]
if event == "ADDON_LOADED" then
-- Need to check arg1 to avoid responding to this event for other addons.
if arg1 == "Bagshui" then
-- This fork requires ClassicAPI (https://github.com/brues-code/ClassicAPI).
-- Bail out before touching SavedVariables so nothing is lost if the DLL isn't loaded.
if not _G.CLASSIC_API_VERSION or not _G.C_Container or not _G.C_Item then
self.classicApiMissing = true
self.eventFrame:UnregisterAllEvents()
_G.DEFAULT_CHAT_FRAME:AddMessage(
"|cffdb4e97Bagshui|r |cffff4040requires ClassicAPI.|r "
.. "Install ClassicAPI.dll (github.com/brues-code/ClassicAPI), add it to dlls.txt and launch via VanillaFixes. "
.. "Bagshui is disabled until then; your settings are untouched."
)
return
end
self:AddonLoaded()
self:LoadComponents()
else
return
end
end
if event == "PLAYER_LOGIN" then
-- Do compatibility check after everything else is definitely loaded.
if not self.startupCompatChecked then
self:QueueClassCallback(self, self.CheckCompat, 1)
self.startupCompatChecked = true
end
end
if event == "BAGSHUI_CORE_EVENT_FUNCTIONS_LOADED" then
-- Part of Init() is registering events so we can't run that until event functions are available.
self:Init()
end
if event == "BAGSHUI_LOCALIZATION_LOADED" then
-- Some startup stuff requires localization to be available.
self:PostLocalizationInit()
end
if event == "PLAYER_LOGOUT" then
-- Used by `Inventory:UpdateCache()` to move `item.bagshuiDate` forward
-- so that only in-game time counts for stock states.
self.currentCharacterData.lastLogout = _G.time()
-- Make sure other accounts see this character's final state.
if self.components.Catalog and self.components.Catalog.WriteSharedCharacter then
self.components.Catalog:WriteSharedCharacter(true)
end
end
-- Pass events on to consumers.
if downstreamEvent then
for consumer, events in pairs(self.eventConsumers) do
if events[event] then
consumer[self.eventConsumers[consumer]._eventFunctionName](consumer, downstreamEvent, arg1, arg2, arg3, arg4)
end
end
end
-- Provide an event that happens just after ADDON_LOADED but before anything else
-- so that components split across multiple files can complete initialization.
if downstreamEvent == "ADDON_LOADED" then
self:RaiseEvent("BAGSHUI_ADDON_LOADED")
end
end
-- Component storage.
-- Doesn't need to be exposed; it's up to individual components to expose themselves on
-- `Bagshui.components`, `Bagshui.prototypes`, and/or `Bagshui.environment` as appropriate.
---@type function[]
local components = {}
--- Load a component by executing its function within the Bagshui environment.
--- This must be here because it's needed by every other file.
---@param creationFunction function
---@param loadEvent string? Event to raise after `creationFunction` has run.
function Bagshui:LoadComponent(creationFunction, loadEvent)
setfenv(creationFunction, self.environment)
creationFunction()
if loadEvent and self.RaiseEvent then
self:RaiseEvent(loadEvent)
end
end
--- Queue the creation of a Bagshui component.
--- Components will be loaded FIFO, so load order is controlled by the TOC/XML.
---@param creationFunction function Component code to execute within the Bagshui environment.
function Bagshui:AddComponent(creationFunction)
table.insert(components, creationFunction)
end
--- Execute the component creation functions queued by AddComponent.
function Bagshui:LoadComponents()
for _, creationFunction in ipairs(components) do
self:LoadComponent(creationFunction)
end
end
--- Add additional constants to the Bagshui environment. Keys in the `constantTable`
--- should start with `BS_` and will be prefixed with it if they don't.
---@param constantTable table<string, any> List of `{ name = value }`.
function Bagshui:AddConstants(constantTable)
for name, value in pairs(constantTable) do
local constantName = string.find(name, "^BS_") and name or "BS_" .. name
if self.environment[constantName] then
self:PrintError("Constant " .. constantName .. " is already defined as " .. tostring(value))
else
self.environment[constantName] = value
end
end
end
-- lua-language-server hack to remove "undefined-field" errors when accessing WoW API via _G.
---@class _G
---@field [string] any