├── .pkgmeta ├── embeds.xml ├── SimpleItemLevel.toc ├── README.md ├── .github └── workflows │ └── package.yml ├── api.lua ├── .luacheckrc ├── config.lua └── addon.lua /.pkgmeta: -------------------------------------------------------------------------------- 1 | package-as: SimpleItemLevel 2 | 3 | ignore: 4 | - lib/.gitkeep 5 | 6 | externals: 7 | lib/LibStub: 8 | url: svn://svn.wowace.com/wow/libstub/mainline/trunk 9 | tag: latest 10 | lib/LibAppropriateItems-1.0: git://git.wowace.com/wow/libappropriateitems-1-0/mainline.git 11 | -------------------------------------------------------------------------------- /embeds.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /SimpleItemLevel.toc: -------------------------------------------------------------------------------- 1 | ## Interface: 120001, 110207, 50503, 30405, 11508 2 | ## Title: Simple Item Level 3 | ## Author: Kemayo 4 | ## Notes: Show item levels in useful places 5 | ## OptionalDeps: LibAppropriateItems-1.0 6 | ## SavedVariables: SimpleItemLevelDB 7 | ## Version: @project-version@ 8 | ## IconAtlas: poi-door-arrow-up 9 | 10 | ## Category-enUS: Inventory 11 | ## Category-deDE: Inventar 12 | ## Category-esES: Inventario 13 | ## Category-esMX: Inventario 14 | ## Category-frFR: Inventaire 15 | ## Category-itIT: Inventario 16 | ## Category-koKR: 소지품 17 | ## Category-ptBR: Inventário 18 | ## Category-ruRU: Предметы 19 | ## Category-zhCN: 物品栏 20 | ## Category-zhTW: 物品欄 21 | 22 | ## X-Website: https://www.wowace.com/projects/simple-item-level 23 | ## X-License: BSD 24 | ## X-Curse-Project-ID: 287692 25 | ## X-Wago-ID: WYK9Da6L 26 | 27 | #@no-lib-strip@ 28 | embeds.xml 29 | #@end-no-lib-strip@ 30 | 31 | addon.lua 32 | config.lua 33 | api.lua 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Item Levels 2 | 3 | Show item levels on: 4 | 5 | * The character panel 6 | * The inspect panel 7 | * Loot windows 8 | * The equipment-picker flyout 9 | * Weapons, armor, and artifact relics in bags (built in, bagnon, baggins, inventorian) 10 | * Tooltips (in classic) 11 | 12 | I'm open to adding them in more places. 13 | 14 | Also shows: 15 | 16 | * An upgrade arrow on items in your bags which you can use whose item level is higher than whatever you currently have equipped. 17 | * A soulbound indicator, so you can see whether something is soulbound, bind-on-equip, or warbound-until-equipped 18 | * Missing enchants and gems 19 | 20 | ### Simple configuration 21 | 22 | For a summary of settings: 23 | ```/simpleilvl``` 24 | 25 | To toggle a place to display item levels: 26 | ```/simpleilvl [type]``` 27 | 28 | ...where `type` is `bags`, `character`, or `inspect`. 29 | 30 | To disable the upgrade arrow: 31 | ```/simpleilvl upgrades``` 32 | 33 | To change whether the text is colored by item quality or just left white: 34 | ```/simpleilvl color``` -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package Addon 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ '*' ] 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install luarocks 14 | run: sudo apt-get install luarocks 15 | - name: Install luacheck 16 | run: luarocks install --local luacheck 17 | - name: Run luacheck 18 | run: ~/.luarocks/bin/luacheck . --no-color -q 19 | build: 20 | needs: lint 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 # reads history for commit changelog 28 | 29 | - name: Create Retail Package 30 | uses: BigWigsMods/packager@master 31 | with: 32 | args: -S 33 | env: 34 | CF_API_KEY: ${{ secrets.CF_API_KEY }} 35 | WOWI_API_TOKEN: ${{ secrets.WOWI_API_TOKEN }} 36 | WAGO_API_TOKEN: ${{ secrets.WAGO_API_TOKEN }} 37 | GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /api.lua: -------------------------------------------------------------------------------- 1 | local myname, ns = ... 2 | local continuableContainer 3 | 4 | _G.SimpleItemLevel.API = {} 5 | 6 | 7 | local function itemFromArg(item) 8 | -- take an Item or item link and returns a non-empty Item or nil 9 | if not item then return end 10 | if type(item) == "string" then 11 | item = Item:CreateFromItemLink(item) 12 | end 13 | if item:IsItemEmpty() then 14 | return 15 | end 16 | return item 17 | end 18 | 19 | 20 | -- Finds the item level for an item 21 | -- 22 | -- This is almost the same as item:GetCurrentItemLevel, but it handles 23 | -- giving caged battle pets a level as well. 24 | -- 25 | -- `item` is an item link or an Item. Note: an Item created from 26 | -- an itemID may be inaccurate due to item scaling. 27 | -- Returns number or nil 28 | SimpleItemLevel.API.ItemLevel = function(item) 29 | item = itemFromArg(item) 30 | local details = ns.DetailsFromItemInstant(item) 31 | return details.level 32 | end 33 | 34 | 35 | -- Colorizes an item's level 36 | -- 37 | -- `item` is an item link or an Item. Note: an Item created from 38 | -- an itemID may be inaccurate due to item scaling. 39 | -- Returns string, will be "|cffffffff?|r" if an invalid item is given 40 | SimpleItemLevel.API.ItemLevelColorized = function(item) 41 | item = itemFromArg(item) 42 | local details = ns.DetailsFromItemInstant(item) 43 | local color = ITEM_QUALITY_COLORS[details.quality or 1] 44 | return color.hex .. (details.level or "?") .. "|r" 45 | end 46 | 47 | 48 | -- Tests whether an item is an upgrade compared to current equipment 49 | -- 50 | -- `item` is an item link or an Item. Note: an Item created from 51 | -- an itemID may be inaccurate due to item scaling. 52 | -- Returns boolean 53 | SimpleItemLevel.API.ItemIsUpgrade = function(item) 54 | item = itemFromArg(item) 55 | return ns.ItemIsUpgrade(item) 56 | end 57 | 58 | -- Tests whether an item is an upgrade compared to current equipment 59 | -- 60 | -- Does all necessary data-caching for the items involved. This has more 61 | -- overhead, but guarantees that you'll get an accurate result. 62 | -- 63 | -- `item` is an item link or an Item. Note: an Item created from 64 | -- an itemID may be inaccurate due to item scaling. 65 | -- `callback` is a function which will be passed a boolean `isUpgrade` 66 | SimpleItemLevel.API.ItemIsUpgradeAsync = function(item, callback) 67 | if not continuableContainer then 68 | continuableContainer = ContinuableContainer:Create() 69 | end 70 | item = itemFromArg(item) 71 | if not item then 72 | return callback(false) 73 | end 74 | 75 | continuableContainer:AddContinuable(item) 76 | 77 | local _, _, _, equipLoc = C_Item.GetItemInfoInstant(item:GetItemID()) 78 | ns.ForEquippedItems(equipLoc, function(equippedItem, slot) 79 | if not equippedItem:IsItemEmpty() then 80 | continuableContainer:AddContinuable(equippedItem) 81 | end 82 | end) 83 | 84 | continuableContainer:ContinueOnLoad(function() 85 | callback(SimpleItemLevel.API.ItemIsUpgrade(item)) 86 | end) 87 | end 88 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "lua51" 2 | max_line_length = false 3 | exclude_files = { 4 | "libs/", 5 | ".luacheckrc" 6 | } 7 | 8 | ignore = { 9 | "211", -- Unused local variable 10 | "212", -- Unused argument 11 | "213", -- Unused loop variable 12 | "231", -- Local variable is set but never accessed 13 | "311", -- Value assigned to a local variable is unused 14 | "542", -- empty if branch 15 | } 16 | 17 | globals = { 18 | "SimpleItemLevel", 19 | "SlashCmdList", 20 | "StaticPopupDialogs", 21 | "UpdateContainerFrameAnchors", 22 | "Baggins", 23 | "Bagnon", 24 | "Combuctor", 25 | "Bagnonium", 26 | "Baganator", 27 | "Baganator_SingleViewBackpackViewFrame", 28 | "Baganator_SingleViewBankViewFrame", 29 | "Baganator_BackpackViewFrame", 30 | "Baganator_BankViewFrame", 31 | } 32 | 33 | read_globals = { 34 | "bit", 35 | "ceil", "floor", 36 | "mod", 37 | "max", 38 | "table", "tinsert", "wipe", "copy", "tContains", 39 | "string", "tostringall", "strtrim", "strmatch", "strsplit", 40 | "pcall", "xpcall", "geterrorhandler", 41 | 42 | -- our own globals 43 | 44 | -- misc custom, third party libraries 45 | "LibStub", "tekDebug", 46 | "GetAuctionBuyout", 47 | 48 | -- API functions 49 | "C_AddOns", 50 | "C_Bank", 51 | "C_Item", 52 | "C_TooltipInfo", 53 | "hooksecurefunc", 54 | "BankButtonIDToInvSlotID", 55 | "ContainerIDToInventoryID", 56 | "ReagentBankButtonIDToInvSlotID", 57 | "CreateAtlasMarkup", 58 | "CursorHasItem", 59 | "DeleteCursorItem", 60 | "GetAuctionItemSubClasses", 61 | "GetBuildInfo", 62 | "GetBackpackAutosortDisabled", 63 | "GetBagSlotFlag", 64 | "GetBankAutosortDisabled", 65 | "GetBankBagSlotFlag", 66 | "GetContainerNumFreeSlots", 67 | "GetContainerNumSlots", 68 | "GetContainerItemID", 69 | "GetContainerItemInfo", 70 | "GetContainerItemLink", 71 | "GetCurrentGuildBankTab", 72 | "GetCursorInfo", 73 | "GetDetailedItemLevelInfo", 74 | "GetGuildBankItemInfo", 75 | "GetGuildBankItemLink", 76 | "GetGuildBankTabInfo", 77 | "GetGuildBankNumSlots", 78 | "GetInspectSpecialization", 79 | "GetInventoryItemID", 80 | "GetInventoryItemLink", 81 | "GetInventoryItemQuality", 82 | "GetInventorySlotInfo", 83 | "GetItemClassInfo", 84 | "GetItemFamily", 85 | "GetItemInfo", 86 | "GetItemInfoInstant", 87 | "GetItemStats", 88 | "GetLootSlotLink", 89 | "GetTime", 90 | "GetVoidItemHyperlinkString", 91 | "GetVoidItemInfo", 92 | "InCombatLockdown", 93 | "IsAltKeyDown", 94 | "IsControlKeyDown", 95 | "IsShiftKeyDown", 96 | "IsSpellKnown", 97 | "IsReagentBankUnlocked", 98 | "Item", 99 | "PlaySound", 100 | "PickupContainerItem", 101 | "PickupGuildBankItem", 102 | "QueryGuildBankTab", 103 | "SetItemButtonTexture", 104 | "SplitContainerItem", 105 | "SplitGuildBankItem", 106 | "UnitClass", 107 | "UnitIsAFK", 108 | "UnitLevel", 109 | "UnitName", 110 | "UseContainerItem", 111 | 112 | -- FrameXML frames 113 | "AccountBankPanel", 114 | "BankFrame", 115 | "GuildBankFrame", 116 | "InspectFrame", 117 | "PaperDollFrame", 118 | "CharacterLevelText", 119 | "InspectLevelText", 120 | "CharacterModelScene", 121 | "CharacterModelFrame", 122 | "InspectModelFrame", 123 | "MerchantFrame", 124 | "GameTooltip", 125 | "ItemRefTooltip", 126 | "LootFrame", 127 | "UIParent", 128 | "WorldFrame", 129 | "DEFAULT_CHAT_FRAME", 130 | "GameFontHighlightSmall", 131 | "GameFontNormalOutline", 132 | "GameFontNormalLargeOutline", 133 | "GameFontNormalHugeOutline", 134 | "NumberFontNormal", 135 | "NumberFontNormalSmall", 136 | "VoidStorageFrame", 137 | "ContainerFrameCombinedBags", 138 | "ContainerFrameContainer", 139 | "InterfaceOptionsFramePanelContainer", 140 | "EquipmentFlyoutFrame", 141 | 142 | -- FrameXML API 143 | "Enum", 144 | "CreateFrame", 145 | "ToggleDropDownMenu", 146 | "UIDropDownMenu_AddButton", 147 | "UIDropDownMenu_CreateInfo", 148 | "UIDropDownMenu_Initialize", 149 | "UIDropDownMenu_SetFrameStrata", 150 | "UIDropDownMenu_SetSelectedValue", 151 | "UIDropDownMenu_SetWidth", 152 | "UISpecialFrames", 153 | "ScrollingEdit_OnCursorChanged", 154 | "ScrollingEdit_OnUpdate", 155 | "InspectPaperDollFrame_OnShow", 156 | "InspectPaperDollFrame_UpdateButtons", 157 | "EquipmentManager_UnpackLocation", 158 | "EquipmentManager_GetItemInfoByLocation", 159 | "EquipmentManager_GetLocationData", 160 | "ContainerFrameItemButton_CalculateItemTooltipAnchors", 161 | "GameTooltip_Hide", 162 | "Settings", 163 | "TooltipDataProcessor", 164 | "ContinuableContainer", 165 | "MergeTable", 166 | "LinkUtil", 167 | 168 | -- FrameXML Constants 169 | "APPEARANCE_LABEL", 170 | "BACKPACK_CONTAINER", 171 | "BACKPACK_TOOLTIP", 172 | "BAG_CLEANUP_BAGS", 173 | "BAG_FILTER_ICONS", 174 | "BAGSLOT", 175 | "BAGSLOTTEXT", 176 | "BANK", 177 | "BANK_BAG_PURCHASE", 178 | "BANK_CONTAINER", 179 | "CONFIRM_BUY_BANK_SLOT", 180 | "DEFAULT", 181 | "DISPLAY_HEADER", 182 | "EQUIP_CONTAINER", 183 | "EQUIPMENTFLYOUT_FIRST_SPECIAL_LOCATION", 184 | "INSPECT", 185 | "INVSLOT_FIRST_EQUIPPED", 186 | "INVSLOT_LAST_EQUIPPED", 187 | "INVSLOT_BODY", 188 | "INVSLOT_TABARD", 189 | "INVSLOT_MAINHAND", 190 | "INVSLOT_OFFHAND", 191 | "ITEM_BIND_QUEST", 192 | "ITEM_BNETACCOUNTBOUND", 193 | "ITEM_CONJURED", 194 | "ITEM_LEVEL", 195 | "ITEM_QUALITY_COLORS", 196 | "ITEM_SOULBOUND", 197 | "LE_BAG_FILTER_FLAG_EQUIPMENT", 198 | "LE_BAG_FILTER_FLAG_IGNORE_CLEANUP", 199 | "LE_EXPANSION_LEVEL_CURRENT", 200 | "LE_EXPANSION_MISTS_OF_PANDARIA", 201 | "LE_ITEM_CLASS_WEAPON", 202 | "LE_ITEM_CLASS_ARMOR", 203 | "LE_ITEM_CLASS_CONTAINER", 204 | "LE_ITEM_CLASS_GEM", 205 | "LE_ITEM_CLASS_ITEM_ENHANCEMENT", 206 | "LE_ITEM_CLASS_CONSUMABLE", 207 | "LE_ITEM_CLASS_GLYPH", 208 | "LE_ITEM_CLASS_TRADEGOODS", 209 | "LE_ITEM_CLASS_RECIPE", 210 | "LE_ITEM_CLASS_BATTLEPET", 211 | "LE_ITEM_CLASS_QUESTITEM", 212 | "LE_ITEM_CLASS_MISCELLANEOUS", 213 | "LE_ITEM_GEM_ARTIFACTRELIC", 214 | "LE_ITEM_QUALITY_POOR", 215 | "LE_ITEM_QUALITY_UNCOMMON", 216 | "LOOT", 217 | "MAX_CONTAINER_ITEMS", 218 | "NEW_ITEM_ATLAS_BY_QUALITY", 219 | "NO", 220 | "NUM_BAG_SLOTS", 221 | "NUM_BANKBAGSLOTS", 222 | "NUM_CONTAINER_FRAMES", 223 | "NUM_LE_BAG_FILTER_FLAGS", 224 | "ORDER_HALL_EQUIPMENT_SLOTS", 225 | "RAID_CLASS_COLORS", 226 | "REAGENT_BANK", 227 | "REAGENTBANK_CONTAINER", 228 | "REAGENTBANK_DEPOSIT", 229 | "RED_FONT_COLOR", 230 | "REMOVE", 231 | "RETRIEVING_ITEM_INFO", 232 | "SHOW_ITEM_LEVEL", 233 | "SOUNDKIT", 234 | "STATICPOPUP_NUMDIALOGS", 235 | "TEXTURE_ITEM_QUEST_BANG", 236 | "TEXTURE_ITEM_QUEST_BORDER", 237 | "UIDROPDOWNMENU_MENU_VALUE", 238 | "WOW_PROJECT_ID", 239 | "WOW_PROJECT_MAINLINE", 240 | "YES", 241 | } 242 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | local myname, ns = ... 2 | local myfullname = C_AddOns.GetAddOnMetadata(myname, "Title") 3 | 4 | local isClassic = WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE 5 | 6 | local function makeFontString(frame, label, indented) 7 | local text = frame:CreateFontString(nil, "OVERLAY", indented and "GameFontNormalSmall" or "GameFontNormal") 8 | text:SetJustifyH("LEFT") 9 | text:SetText(label) 10 | if indented then 11 | text:SetPoint("LEFT", frame, (15 + 37), 0) -- indent variant 12 | else 13 | text:SetPoint("LEFT", frame, 37, 0) 14 | end 15 | text:SetPoint("RIGHT", frame, "CENTER", -85, 0) 16 | 17 | return text 18 | end 19 | 20 | local function makeTitle(parent, text) 21 | local title = CreateFrame("Frame", nil, parent) 22 | title.Text = makeFontString(title, text) 23 | title:SetSize(280, 26) 24 | title:SetPoint("RIGHT", parent) 25 | return title 26 | end 27 | 28 | local function makeSlider(parent, key, label, minValue, maxValue, step, formatter, callback, indented) 29 | local frame = CreateFrame("Frame", nil, parent) 30 | -- frame:EnableMouse(true) 31 | frame.Slider = CreateFrame("Slider", nil, frame) 32 | frame.Slider:SetObeyStepOnDrag(true) 33 | frame.Slider:SetOrientation("HORIZONTAL") 34 | frame.Slider.Left = frame.Slider:CreateTexture() 35 | frame.Slider.Left:SetPoint("LEFT") 36 | frame.Slider.Right = frame.Slider:CreateTexture() 37 | frame.Slider.Right:SetPoint("RIGHT") 38 | frame.Slider.Middle = frame.Slider:CreateTexture() 39 | frame.Slider.Middle:SetPoint("TOPLEFT", frame.Slider.Left, "TOPRIGHT") 40 | frame.Slider.Middle:SetPoint("TOPRIGHT", frame.Slider.Right, "TOPLEFT") 41 | frame.Slider.Thumb = frame.Slider:CreateTexture() 42 | frame.Slider:SetThumbTexture(frame.Slider.Thumb) 43 | frame.Slider:SetSize(200, 19) 44 | 45 | frame.Slider.Left:SetAtlas("Minimal_SliderBar_Left", true) 46 | frame.Slider.Right:SetAtlas("Minimal_SliderBar_Right", true) 47 | frame.Slider.Middle:SetAtlas("_Minimal_SliderBar_Middle", true) 48 | frame.Slider.Thumb:SetAtlas("Minimal_SliderBar_Button", true) 49 | 50 | -- frame.Slider:SetPoint("TOPLEFT", frame, 19) 51 | -- frame.Slider:SetPoint("BOTTOMRIGHT", frame, -19) 52 | formatter = formatter or function(value) return string.format("%.1f", value) end 53 | frame.FormatValue = function(self, value) 54 | frame.RightText:SetText(formatter(value)) 55 | frame.RightText:Show() 56 | end 57 | frame.Slider:SetScript("OnValueChanged", function(slider, value) 58 | frame:FormatValue(value) 59 | ns.db[key] = value 60 | if callback then callback(key, value) end 61 | end) 62 | frame.Slider:SetScript("OnMouseDown", function(slider) 63 | if slider:IsEnabled() then 64 | PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) 65 | end 66 | end) 67 | frame.OnStepperClicked = function(self, forward) 68 | local value = self.Slider:GetValue() 69 | if forward then 70 | self.Slider:SetValue(value + self.Slider:GetValueStep()) 71 | else 72 | self.Slider:SetValue(value - self.Slider:GetValueStep()) 73 | end 74 | 75 | PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) 76 | end 77 | 78 | frame.Back = CreateFrame("Button", nil, frame) 79 | frame.Back:SetSize(11, 19) 80 | frame.Back:SetPoint("RIGHT", frame.Slider, "LEFT", -4, 0) 81 | frame.Back.Background = frame.Back:CreateTexture(nil, "BACKGROUND") 82 | frame.Back.Background:SetPoint("CENTER") 83 | frame.Back:SetScript("OnClick", function() frame:OnStepperClicked(false) end) 84 | frame.Forward = CreateFrame("Button", nil, frame) 85 | frame.Forward:SetSize(11, 19) 86 | frame.Forward:SetPoint("LEFT", frame.Slider, "RIGHT", 4, 0) 87 | frame.Forward.Background = frame.Forward:CreateTexture(nil, "BACKGROUND") 88 | frame.Forward.Background:SetPoint("CENTER") 89 | frame.Forward:SetScript("OnClick", function() frame:OnStepperClicked(true) end) 90 | 91 | frame.Back.Background:SetAtlas("Minimal_SliderBar_Button_Left", true) 92 | frame.Forward.Background:SetAtlas("Minimal_SliderBar_Button_Right", true) 93 | 94 | frame.RightText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") 95 | frame.RightText:SetPoint("LEFT", frame.Slider, "RIGHT", 25, 0) 96 | 97 | frame.Slider:SetWidth(250) 98 | frame.Slider:SetPoint("LEFT", frame, "CENTER", -80, 3) 99 | frame.Text = makeFontString(frame, label, indented) 100 | frame:SetSize(280, 26) 101 | 102 | frame:SetScript("OnShow", function(self) 103 | if self.initialized then return end 104 | local value = ns.db[key] 105 | local steps = (step and (maxValue - minValue) / step) or 100 106 | self.Slider:SetMinMaxValues(minValue, maxValue) 107 | self.Slider:SetValueStep((maxValue - minValue) / steps) 108 | self.Slider:SetValue(value) 109 | self:FormatValue(value) 110 | self.initialized = true 111 | end) 112 | 113 | frame:SetPoint("RIGHT", parent) 114 | 115 | return frame 116 | end 117 | 118 | local function makeDropdown(parent, key, label, values, callback) 119 | local frame = CreateFrame("Frame", nil, parent) 120 | frame.Dropdown = CreateFrame("Frame", myname .. "Options" .. key .. "Dropdown", frame, "UIDropDownMenuTemplate") 121 | frame.Dropdown:SetPoint("LEFT", frame, "CENTER", -110, 3) 122 | frame.Dropdown:HookScript("OnShow", function() 123 | if frame.initialize then return end 124 | UIDropDownMenu_Initialize(frame.Dropdown, function() 125 | for k, v in pairs(values) do 126 | local info = UIDropDownMenu_CreateInfo() 127 | info.text = v 128 | info.value = k 129 | info.func = function(self) 130 | ns.db[key] = self.value 131 | UIDropDownMenu_SetSelectedValue(frame.Dropdown, self.value) 132 | if callback then callback(key, self.value) end 133 | end 134 | UIDropDownMenu_AddButton(info) 135 | end 136 | UIDropDownMenu_SetSelectedValue(frame.Dropdown, ns.db[key]) 137 | end) 138 | end) 139 | UIDropDownMenu_SetWidth(frame.Dropdown, 280) 140 | 141 | frame.Text = makeFontString(frame, label, true) 142 | 143 | frame:SetPoint("RIGHT", parent) 144 | 145 | frame:SetSize(280, 26) 146 | 147 | return frame 148 | end 149 | 150 | local makeCheckbox 151 | do 152 | local function checkboxGetValue(self) return ns.db[self.key] end 153 | local function checkboxSetChecked(self) self:SetChecked(self:GetValue()) end 154 | local function checkboxSetValue(self, checked) 155 | ns.db[self.key] = checked 156 | if self.callback then self.callback(self.key, checked) end 157 | end 158 | local function checkboxOnClick(self) 159 | local checked = self:GetChecked() 160 | PlaySound(checked and SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF) 161 | self:SetValue(checked) 162 | end 163 | local function checkboxOnEnter(self) 164 | if self.tooltipText then 165 | GameTooltip:SetOwner(self, self.tooltipOwnerPoint or "ANCHOR_RIGHT") 166 | GameTooltip:SetText(self.tooltipText, nil, nil, nil, nil, true) 167 | end 168 | if self.tooltipRequirement then 169 | GameTooltip:AddLine(self.tooltipRequirement, 1.0, 1.0, 1.0, true) 170 | GameTooltip:Show() 171 | end 172 | end 173 | function makeCheckbox(parent, key, label, description, callback) 174 | local frame = CreateFrame("Frame", nil, parent) 175 | local check = CreateFrame("CheckButton", nil, frame, "InterfaceOptionsCheckButtonTemplate") 176 | check.key = key 177 | check.callback = callback 178 | check.GetValue = checkboxGetValue 179 | check.SetValue = checkboxSetValue 180 | check:SetScript('OnShow', checkboxSetChecked) 181 | check:SetScript("OnClick", checkboxOnClick) 182 | check:SetScript("OnEnter", checkboxOnEnter) 183 | check:SetScript("OnLeave", GameTooltip_Hide) 184 | check.tooltipText = label 185 | check.tooltipRequirement = description 186 | check:SetPoint("LEFT", frame, "CENTER", -90, 0) 187 | frame.Check = check 188 | 189 | frame.Text = makeFontString(frame, label, true) 190 | 191 | frame:SetPoint("RIGHT", parent) 192 | 193 | frame:SetSize(280, 26) 194 | 195 | return frame 196 | end 197 | end 198 | local function makeCheckboxList(parent, checkboxes, previous, callback) 199 | for _, data in ipairs(checkboxes) do 200 | local control 201 | if data[1] then 202 | control = makeCheckbox(parent, data[1], data[2], data[3], callback) 203 | else 204 | control = makeTitle(parent, data[2]) 205 | end 206 | control:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -4) 207 | previous = control 208 | end 209 | return previous 210 | end 211 | 212 | local function button_onenter(self) 213 | GameTooltip:SetOwner(self, "ANCHOR_NONE") 214 | ContainerFrameItemButton_CalculateItemTooltipAnchors(self, GameTooltip) 215 | 216 | local link = self:GetItemLink() 217 | if link then 218 | GameTooltip:SetHyperlink(self:GetItemLink()) 219 | else 220 | GameTooltip:AddLine(RETRIEVING_ITEM_INFO, 1, 0, 0) 221 | end 222 | 223 | GameTooltip:Show() 224 | end 225 | local function makeItemButton(parent) 226 | local button = CreateFrame(isClassic and "BUTTON" or "ItemButton", nil, parent, isClassic and "ItemButtonTemplate" or nil) 227 | -- classic 228 | if not button.SetItem then 229 | function button:SetItem(item) 230 | local itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subclassID = C_Item.GetItemInfoInstant(item) 231 | if itemID then 232 | self.itemID = itemID 233 | SetItemButtonTexture(button, icon) 234 | end 235 | end 236 | function button:GetItemID() 237 | return self.itemID 238 | end 239 | function button:GetItemLink() 240 | return select(2, C_Item.GetItemInfo(self.itemID)) 241 | end 242 | end 243 | button:SetScript("OnEnter", button_onenter) 244 | button:SetScript("OnLeave", GameTooltip_Hide) 245 | return button 246 | end 247 | 248 | local function makeConfigPanel(id, name, parent, parentname) 249 | local frame 250 | 251 | frame = CreateFrame("Frame") 252 | frame.OnCommit = function() end 253 | frame.OnDefault = function() end 254 | frame.OnRefresh = function() end 255 | 256 | local category, layout 257 | if parent then 258 | local parentcategory = Settings.GetCategory(parent) 259 | category, layout = Settings.RegisterCanvasLayoutSubcategory(parentcategory, frame, name) 260 | else 261 | category, layout = Settings.RegisterCanvasLayoutCategory(frame, name) 262 | Settings.RegisterAddOnCategory(category) 263 | end 264 | category.ID = id 265 | layout:AddAnchorPoint("TOPLEFT", 10, -10) 266 | layout:AddAnchorPoint("BOTTOMRIGHT", -10, 10) 267 | 268 | frame:Hide() 269 | return frame 270 | end 271 | 272 | -- actual config panel: 273 | function ns:SetupConfig() 274 | local demoButtons = {} 275 | local function refresh(_, value) 276 | ns.RefreshOverlayFrames() 277 | for itemID, button in pairs(demoButtons) do 278 | ns.CleanButton(button) 279 | ns.UpdateButtonFromItem(button, Item:CreateFromItemID(itemID), "character") 280 | end 281 | end 282 | 283 | do 284 | local frame = makeConfigPanel(myname, myfullname) 285 | local title = makeTitle(frame, SHOW_ITEM_LEVEL) 286 | title:SetPoint("TOPLEFT", frame) 287 | 288 | local checkboxes = { 289 | {"bags", BAGSLOTTEXT}, 290 | {"character", ORDER_HALL_EQUIPMENT_SLOTS}, 291 | {"flyout", "Equipment flyouts"}, 292 | {"inspect", INSPECT}, 293 | {"loot", LOOT}, 294 | {"characteravg", "Character average item level"}, 295 | {"inspectavg", "Inspect average item level"}, 296 | } 297 | if isClassic or ns.db.tooltip then 298 | table.insert(checkboxes, {"tooltip", "Item tooltips", "Add the item level to tooltips"}) 299 | end 300 | 301 | local last = makeCheckboxList(frame, checkboxes, title, refresh) 302 | 303 | last = makeCheckboxList(frame, { 304 | {false, "Selectiveness"}, 305 | {"equipment", "Show on equippable items"}, 306 | {"battlepets", "Show on battle pets"}, 307 | {"reagents", "Show on crafting reagents"}, 308 | {"misc", "Show on anything else"}, 309 | }, last, refresh) 310 | 311 | local values = {} 312 | for label, value in pairs(Enum.ItemQuality) do 313 | values[value] = label 314 | end 315 | local quality = makeDropdown(frame, "quality", "Minimum item quality to show", values, refresh) 316 | quality:SetPoint("TOPLEFT", last, "BOTTOMLEFT", 0, -4) 317 | 318 | -- Settings.OpenToCategory(myname) 319 | end 320 | 321 | do 322 | local frame = makeConfigPanel(myname.."_appearance", APPEARANCE_LABEL, myname, myfullname) 323 | local demo = CreateFrame("Frame", nil, frame) 324 | 325 | demo:SetPoint("TOPLEFT", frame) 326 | demo:SetPoint("RIGHT", frame) 327 | demo:SetHeight(43) 328 | 329 | demo:SetScript("OnShow", function() 330 | local previousButton 331 | for _, itemID in ipairs(isClassic and {19019, 19364, 10328, 11122, 23192, 7997, 14047} or {120978, 186414, 195527, 194065, 197957, 77256, 86079, 44168}) do 332 | local button = makeItemButton(demo) 333 | if not previousButton then 334 | button:SetPoint("TOPLEFT", 112, -2) 335 | else 336 | button:SetPoint("TOPLEFT", previousButton, "TOPRIGHT", 2, 0) 337 | end 338 | button:SetItem(itemID) 339 | ns.UpdateButtonFromItem(button, Item:CreateFromItemID(itemID), "character") 340 | demoButtons[itemID] = button 341 | previousButton = button 342 | end 343 | demo:SetScript("OnShow", nil) 344 | end) 345 | 346 | local title = makeTitle(frame, APPEARANCE_LABEL) 347 | -- title:SetPoint("TOPLEFT", frame) 348 | title:SetPoint("TOPLEFT", demo, "BOTTOMLEFT", 0, -4) 349 | 350 | local fonts = {} 351 | for k,v in pairs(ns.Fonts) do 352 | fonts[k] = k 353 | end 354 | local font = makeDropdown(frame, "font", "Font", fonts, refresh) 355 | font:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4) 356 | 357 | local positions = {} 358 | for k,v in pairs(ns.PositionOffsets) do 359 | positions[k] = k 360 | end 361 | local position = makeDropdown(frame, "position", "Position of item level", positions, refresh) 362 | position:SetPoint("TOPLEFT", font, "BOTTOMLEFT", 0, -4) 363 | local positionup = makeDropdown(frame, "positionup", "Position of upgrade indicator", positions, refresh) 364 | positionup:SetPoint("TOPLEFT", position, "BOTTOMLEFT", 0, -4) 365 | 366 | local positionmissing = makeDropdown(frame, "positionmissing", "Position of missing indicator", positions, refresh) 367 | positionmissing:SetPoint("TOPLEFT", positionup, "BOTTOMLEFT", 0, -4) 368 | local scaleup = makeSlider(frame, "scaleup", "Size of upgrade indicator", 0.5, 3, 0.1, nil, refresh, true) 369 | scaleup:SetPoint("TOPLEFT", positionmissing, "BOTTOMLEFT", 0, -4) 370 | 371 | local positionbound = makeDropdown(frame, "positionbound", "Position of soulbound indicator", positions, refresh) 372 | positionbound:SetPoint("TOPLEFT", scaleup, "BOTTOMLEFT", 0, -4) 373 | local scalebound = makeSlider(frame, "scalebound", "Size of soulbound indicator", 0.5, 3, 0.1, nil, refresh, true) 374 | scalebound:SetPoint("TOPLEFT", positionbound, "BOTTOMLEFT", 0, -4) 375 | 376 | makeCheckboxList(frame, { 377 | {false, DISPLAY_HEADER}, 378 | {"itemlevel", SHOW_ITEM_LEVEL, "Do you want to disable the core feature of this addon? Maybe."}, 379 | {"upgrades", ("Flag upgrade items (%s)"):format(ns.upgradeString)}, 380 | {"missinggems", ("Flag items missing gems (%s)"):format(ns.gemString)}, 381 | {"missingenchants", ("Flag items missing enchants (%s)"):format(ns.enchantString)}, 382 | {"missingcharacter", "...missing gems/enchants on the character frame only?"}, 383 | {"bound", ("Flag items that are %s (%s)"):format(ITEM_SOULBOUND, CreateAtlasMarkup(ns.soulboundAtlas)), "Only on items you control; bags and character"}, 384 | {"color", "Color item level by item quality"}, 385 | }, scalebound, refresh) 386 | end 387 | end 388 | 389 | -- Quick config: 390 | 391 | _G["SLASH_".. myname:upper().."1"] = "/simpleilvl" 392 | SlashCmdList[myname:upper()] = function(msg) 393 | msg = msg:trim() 394 | if msg:match("^quality") then 395 | local quality = msg:match("quality (.+)") or "" 396 | if quality:match("^%d+$") then 397 | quality = tonumber(quality) 398 | else 399 | quality = quality:lower() 400 | for label, value in pairs(Enum.ItemQuality) do 401 | if label:lower() == quality then 402 | quality = value 403 | end 404 | end 405 | end 406 | if type(quality) ~= "number" then 407 | return ns.Print("Invalid item quality provided, should be a name or a number 0-8") 408 | end 409 | ns.db.quality = quality 410 | return ns.Print("quality = ", _G["ITEM_QUALITY" .. ns.db.quality .. "_DESC"]) 411 | end 412 | if ns.db[msg] ~= nil then 413 | ns.db[msg] = not ns.db[msg] 414 | return ns.Print(msg, '=', ns.db[msg] and YES or NO) 415 | end 416 | if msg == "" then 417 | Settings.OpenToCategory(myname) 418 | end 419 | end 420 | -------------------------------------------------------------------------------- /addon.lua: -------------------------------------------------------------------------------- 1 | local myname, ns = ... 2 | local myfullname = C_AddOns.GetAddOnMetadata(myname, "Title") 3 | local db 4 | local isClassic = WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE 5 | ns.DEBUG = C_AddOns.GetAddOnMetadata(myname, "Version") == "@".."project-version@" 6 | 7 | _G.SimpleItemLevel = {} 8 | 9 | local SLOT_MAINHAND = GetInventorySlotInfo("MainHandSlot") 10 | local SLOT_OFFHAND = GetInventorySlotInfo("SecondaryHandSlot") 11 | 12 | function ns.Print(...) print("|cFF33FF99".. myfullname.. "|r:", ...) end 13 | 14 | -- events 15 | local hooks = {} 16 | local f = CreateFrame("Frame") 17 | f:SetScript("OnEvent", function(self, event, ...) if ns[event] then return ns[event](ns, event, ...) end end) 18 | function ns:RegisterEvent(...) for i=1,select("#", ...) do f:RegisterEvent((select(i, ...))) end end 19 | function ns:UnregisterEvent(...) for i=1,select("#", ...) do f:UnregisterEvent((select(i, ...))) end end 20 | function ns:RegisterAddonHook(addon, callback) 21 | if C_AddOns.IsAddOnLoaded(addon) then 22 | xpcall(callback, geterrorhandler()) 23 | else 24 | hooks[addon] = callback 25 | end 26 | end 27 | 28 | local LAI = LibStub("LibAppropriateItems-1.0") 29 | 30 | ns.soulboundAtlas = isClassic and "AzeriteReady" or "Soulbind-32x32" -- UF-SoulShard-Icon-2x 31 | ns.upgradeAtlas = "poi-door-arrow-up" -- MiniMap-PositionArrowUp? 32 | ns.upgradeString = CreateAtlasMarkup(ns.upgradeAtlas) 33 | ns.gemString = CreateAtlasMarkup(isClassic and "worldquest-icon-jewelcrafting" or "jailerstower-score-gem-tooltipicon") -- Professions-ChatIcon-Quality-Tier5-Cap 34 | ns.enchantString = RED_FONT_COLOR:WrapTextInColorCode("E") 35 | ns.Fonts = { 36 | HighlightSmall = GameFontHighlightSmall, 37 | Normal = GameFontNormalOutline, 38 | Large = GameFontNormalLargeOutline, 39 | Huge = GameFontNormalHugeOutline, 40 | NumberNormal = NumberFontNormal, 41 | NumberNormalSmall = NumberFontNormalSmall, 42 | } 43 | ns.PositionOffsets = { 44 | TOPLEFT = {2, -2}, 45 | TOPRIGHT = {-2, -2}, 46 | BOTTOMLEFT = {2, 2}, 47 | BOTTOMRIGHT = {-2, 2}, 48 | BOTTOM = {0, 2}, 49 | TOP = {0, -2}, 50 | LEFT = {2, 0}, 51 | RIGHT = {-2, 0}, 52 | CENTER = {0, 0}, 53 | } 54 | 55 | ns.defaults = { 56 | -- places 57 | character = true, 58 | inspect = true, 59 | bags = true, 60 | loot = true, 61 | flyout = true, 62 | tooltip = isClassic, 63 | characteravg = isClassic, 64 | inspectavg = true, 65 | -- equipmentonly = true, 66 | equipment = true, 67 | battlepets = true, 68 | reagents = false, 69 | misc = false, 70 | -- data points 71 | itemlevel = true, 72 | upgrades = true, 73 | missinggems = true, 74 | missingenchants = true, 75 | missingcharacter = true, -- missing on character-frame only 76 | bound = true, 77 | -- display 78 | color = true, 79 | -- Retail has Uncommon, BCC/Classic has Good 80 | quality = Enum.ItemQuality.Common or Enum.ItemQuality.Standard, 81 | -- appearance config 82 | font = "NumberNormal", 83 | position = "TOPRIGHT", 84 | positionup = "TOPLEFT", 85 | positionmissing = "LEFT", 86 | positionbound = "BOTTOMLEFT", 87 | scaleup = 1, 88 | scalebound = 1, 89 | } 90 | 91 | function ns:ADDON_LOADED(event, addon) 92 | if hooks[addon] then 93 | xpcall(hooks[addon], geterrorhandler()) 94 | hooks[addon] = nil 95 | end 96 | if addon == myname then 97 | _G[myname.."DB"] = setmetatable(_G[myname.."DB"] or {}, { 98 | __index = ns.defaults, 99 | }) 100 | db = _G[myname.."DB"] 101 | ns.db = db 102 | 103 | ns:SetupConfig() 104 | 105 | -- So our upgrade arrows can work reliably when opening inventories 106 | ns.CacheEquippedItems() 107 | end 108 | end 109 | ns:RegisterEvent("ADDON_LOADED") 110 | 111 | 112 | local function ItemIsUpgrade(item) 113 | if not (item and LAI:IsAppropriate(item:GetItemID())) then 114 | return 115 | end 116 | -- Upgrade? 117 | if item:GetItemLocation() and item:GetItemLocation():IsEquipmentSlot() then 118 | -- This is meant to catch the character frame, to avoid rings/trinkets 119 | -- you've already got equipped showing as an upgrade since they're 120 | -- higher ilevel than your other ring/trinket 121 | return 122 | end 123 | local isUpgrade 124 | local itemLevel = item:GetCurrentItemLevel() or 0 125 | local _, _, _, equipLoc, _, itemClass, itemSubClass = C_Item.GetItemInfoInstant(item:GetItemID()) 126 | ns.ForEquippedItems(equipLoc, function(equippedItem, slot) 127 | -- This *isn't* async, for flow reasons, so if the equipped items 128 | -- aren't yet cached the item might get incorrectly flagged as an 129 | -- upgrade. 130 | if equippedItem:IsItemEmpty() and slot == SLOT_OFFHAND then 131 | local mainhand = GetInventoryItemID("player", SLOT_MAINHAND) 132 | if mainhand then 133 | local invtype = select(4, C_Item.GetItemInfoInstant(mainhand)) 134 | if invtype == "INVTYPE_2HWEAPON" then 135 | return 136 | end 137 | end 138 | end 139 | if not equippedItem:IsItemDataCached() then 140 | -- don't claim an upgrade if we don't know 141 | return 142 | end 143 | -- fallbacks for the item levels; saw complaints of this erroring during initial login for people using Bagnon and AdiBags 144 | local equippedItemLevel = equippedItem:GetCurrentItemLevel() or 0 145 | if equippedItem:IsItemEmpty() or equippedItemLevel < itemLevel then 146 | isUpgrade = true 147 | local minLevel = select(5, C_Item.GetItemInfo(item:GetItemLink() or item:GetItemID())) 148 | if minLevel and minLevel > UnitLevel("player") then 149 | -- not equipable yet 150 | end 151 | end 152 | end) 153 | return isUpgrade 154 | end 155 | ns.ItemIsUpgrade = ItemIsUpgrade 156 | 157 | -- TODO: this is a good candidate for caching results... 158 | local function DetailsFromItemInstant(item) 159 | if not item or item:IsItemEmpty() then return {} end 160 | -- print("DetailsFromItem", item:GetItemLink()) 161 | local itemLevel = item:GetCurrentItemLevel() 162 | local quality = item:GetItemQuality() 163 | local itemLink = item:GetItemLink() 164 | if itemLink and itemLink:match("battlepet:") then 165 | -- special case for caged battle pets 166 | local _, speciesID, level, breedQuality = ns.GetLinkValues(itemLink) 167 | if speciesID and level and breedQuality then 168 | itemLevel = tonumber(level) 169 | quality = tonumber(breedQuality) 170 | end 171 | end 172 | return { 173 | level = itemLevel, 174 | quality = quality, 175 | link = itemLink, 176 | } 177 | end 178 | ns.DetailsFromItemInstant = DetailsFromItemInstant 179 | 180 | local function DetailsFromItem(item) 181 | if not item or item:IsItemEmpty() then return {} end 182 | local details = DetailsFromItemInstant(item) 183 | details.missingGems = ns.ItemHasEmptySlots(details.link) 184 | details.missingEnchants = ns.ItemIsMissingEnchants(details.link) 185 | details.upgrade = ItemIsUpgrade(item) 186 | 187 | if C_Item.IsItemBindToAccountUntilEquip and details.link then 188 | -- 11.0.2 adds this, which works on any item: 189 | details.warboundUntilEquip = C_Item.IsItemBindToAccountUntilEquip(details.link) 190 | end 191 | if item:IsItemInPlayersControl() then 192 | local itemLocation = item:GetItemLocation() 193 | -- this only works on items in our control: 194 | details.warboundUntilEquip = C_Item.IsBoundToAccountUntilEquip and C_Item.IsBoundToAccountUntilEquip(itemLocation) 195 | details.bound = C_Item.IsBound(itemLocation) 196 | if details.bound then 197 | -- As of 11.0.0 blizzard has created Enum.ItemBind entries for 198 | -- warbound, but never uses them. Zepto worked out that we can 199 | -- use "can I put it in the warbank?" as a proxy to distinguish, 200 | -- even when we're not at the bank. 201 | -- TODO: occasionally check whether the bindTypes start getting 202 | -- returned via `select (14, C_Item.GetItemInfo(details.link)) == 7/8/9` 203 | details.warbound = C_Bank and C_Bank.IsItemAllowedInBankType and C_Bank.IsItemAllowedInBankType(Enum.BankType.Account, itemLocation) 204 | end 205 | end 206 | 207 | return details 208 | end 209 | ns.DetailsFromItem = DetailsFromItem 210 | 211 | ns.frames = {} -- TODO: should I make this a FramePool now? 212 | local function PrepareItemButton(button) 213 | if not button.simpleilvl then 214 | local overlayFrame = CreateFrame("FRAME", nil, button) 215 | overlayFrame:SetAllPoints() 216 | overlayFrame:SetFrameLevel(button:GetFrameLevel() + 1) 217 | button.simpleilvloverlay = overlayFrame 218 | 219 | button.simpleilvl = overlayFrame:CreateFontString(nil, "OVERLAY") 220 | button.simpleilvl:Hide() 221 | 222 | button.simpleilvlup = overlayFrame:CreateTexture(nil, "OVERLAY") 223 | button.simpleilvlup:SetSize(10, 10) 224 | button.simpleilvlup:SetAtlas(ns.upgradeAtlas) 225 | button.simpleilvlup:Hide() 226 | 227 | button.simpleilvlmissing = overlayFrame:CreateFontString(nil, "OVERLAY") 228 | button.simpleilvlmissing:Hide() 229 | 230 | button.simpleilvlbound = overlayFrame:CreateTexture(nil, "OVERLAY") 231 | button.simpleilvlbound:SetSize(10, 10) 232 | button.simpleilvlbound:SetAtlas(ns.soulboundAtlas) -- Soulbind-32x32 233 | button.simpleilvlbound:Hide() 234 | 235 | ns.frames[button] = overlayFrame 236 | end 237 | button.simpleilvloverlay:SetFrameLevel(button:GetFrameLevel() + 1) 238 | 239 | -- Apply appearance config: 240 | button.simpleilvl:ClearAllPoints() 241 | button.simpleilvl:SetPoint(db.position, unpack(ns.PositionOffsets[db.position])) 242 | button.simpleilvl:SetFontObject(ns.Fonts[db.font] or NumberFontNormal) 243 | -- button.simpleilvl:SetJustifyH('RIGHT') 244 | 245 | button.simpleilvlup:ClearAllPoints() 246 | button.simpleilvlup:SetPoint(db.positionup, unpack(ns.PositionOffsets[db.positionup])) 247 | button.simpleilvlup:SetScale(db.scaleup) 248 | 249 | button.simpleilvlmissing:ClearAllPoints() 250 | button.simpleilvlmissing:SetPoint(db.positionmissing, unpack(ns.PositionOffsets[db.positionmissing])) 251 | button.simpleilvlmissing:SetFont([[Fonts\ARIALN.TTF]], 11, "OUTLINE,MONOCHROME") 252 | 253 | button.simpleilvlbound:ClearAllPoints() 254 | button.simpleilvlbound:SetPoint(db.positionbound, unpack(ns.PositionOffsets[db.positionbound])) 255 | button.simpleilvlbound:SetScale(db.scalebound) 256 | end 257 | ns.PrepareItemButton = PrepareItemButton 258 | 259 | local blank = {} 260 | local function CleanButton(button, suppress) 261 | suppress = suppress or blank 262 | if button.simpleilvl and not suppress.level then button.simpleilvl:Hide() end 263 | if button.simpleilvlup and not suppress.upgrade then button.simpleilvlup:Hide() end 264 | if button.simpleilvlmissing and not suppress.missing then button.simpleilvlmissing:Hide() end 265 | if button.simpleilvlbound and not suppress.bound then button.simpleilvlbound:Hide() end 266 | end 267 | ns.CleanButton = CleanButton 268 | 269 | function ns.RefreshOverlayFrames() 270 | for button in pairs(ns.frames) do 271 | PrepareItemButton(button) 272 | end 273 | end 274 | 275 | local function AddLevelToButton(button, details) 276 | if not (db.itemlevel and details.level) then 277 | return button.simpleilvl:Hide() 278 | end 279 | local r, g, b = C_Item.GetItemQualityColor(db.color and details.quality or 1) 280 | button.simpleilvl:SetText(details.level or '?') 281 | button.simpleilvl:SetTextColor(r, g, b) 282 | button.simpleilvl:Show() 283 | end 284 | local function AddUpgradeToButton(button, details) 285 | if not (db.upgrades and details.upgrade) then 286 | return button.simpleilvlup:Hide() 287 | end 288 | local minLevel = select(5, C_Item.GetItemInfo(details.link)) 289 | if minLevel and minLevel > UnitLevel("player") then 290 | button.simpleilvlup:SetVertexColor(1, 0, 0) 291 | else 292 | button.simpleilvlup:SetVertexColor(1, 1, 1) 293 | end 294 | button.simpleilvlup:Show() 295 | end 296 | local function AddMissingToButton(button, details) 297 | local missingGems = db.missinggems and details.missingGems 298 | local missingEnchants = db.missingenchants and details.missingEnchants 299 | button.simpleilvlmissing:SetFormattedText("%s%s", missingGems and ns.gemString or "", missingEnchants and ns.enchantString or "") 300 | button.simpleilvlmissing:Show() 301 | end 302 | 303 | local function ColorFrameByBinding(frame, details) 304 | -- returns bool, whether is bound in some way 305 | if details.bound then 306 | if details.warbound then 307 | frame:SetVertexColor(0.5, 1, 0) -- green 308 | else 309 | frame:SetVertexColor(1, 1, 1) -- blue 310 | end 311 | return true 312 | elseif details.warboundUntilEquip then 313 | -- once you equip it the label changes to soulbound, but this property remains 314 | frame:SetVertexColor(1, 0.5, 1) -- pale purple 315 | return true 316 | end 317 | return false 318 | end 319 | local function AddBoundToButton(button, details) 320 | if not db.bound then 321 | return button.simpleilvlbound and button.simpleilvlbound:Hide() 322 | end 323 | if ColorFrameByBinding(button.simpleilvlbound, details) then 324 | button.simpleilvlbound:Show() 325 | end 326 | end 327 | local function ShouldShowOnItem(item) 328 | local quality = item:GetItemQuality() or -1 329 | if quality < db.quality then 330 | return false 331 | end 332 | local _, _, _, equipLoc, _, itemClass, itemSubClass = C_Item.GetItemInfoInstant(item:GetItemID()) 333 | if ( 334 | itemClass == Enum.ItemClass.Weapon or 335 | itemClass == Enum.ItemClass.Armor or 336 | (itemClass == Enum.ItemClass.Gem and itemSubClass == Enum.ItemGemSubclass.Artifactrelic) 337 | ) then 338 | return db.equipment 339 | end 340 | if item:GetItemID() == 82800 then 341 | -- Pet Cage 342 | return db.battlepets 343 | end 344 | if select(17, C_Item.GetItemInfo(item:GetItemID())) then 345 | return db.reagents 346 | end 347 | return db.misc 348 | end 349 | local function UpdateButtonFromItem(button, item, variant, suppress, extradetails) 350 | if not item or item:IsItemEmpty() then 351 | return 352 | end 353 | suppress = suppress or blank 354 | item:ContinueOnItemLoad(function() 355 | if not ShouldShowOnItem(item) then return end 356 | PrepareItemButton(button) 357 | local details = DetailsFromItem(item) 358 | if extradetails then MergeTable(details, extradetails) end 359 | if not suppress.level then AddLevelToButton(button, details) end 360 | if not suppress.upgrade then AddUpgradeToButton(button, details) end 361 | if not suppress.bound then AddBoundToButton(button, details) end 362 | if (variant == "character" or variant == "inspect" or not db.missingcharacter) then 363 | -- if item.itemID then print("Skipping missing on", item:GetItemLink()) end 364 | if not (suppress.missing or item.itemID) then 365 | -- If an item was built from just an itemID it cannot know this 366 | -- (And in the inspect case, it's going to get refreshed shortly) 367 | AddMissingToButton(button, details) 368 | end 369 | end 370 | end) 371 | return true 372 | end 373 | ns.UpdateButtonFromItem = UpdateButtonFromItem 374 | 375 | local continuableContainer 376 | local function AddAverageLevelToFontString(unit, fontstring) 377 | if not fontstring then return end 378 | if not continuableContainer then 379 | continuableContainer = ContinuableContainer:Create() 380 | end 381 | fontstring:Hide() 382 | local key = unit == "player" and "character" or "inspect" 383 | if not db[key .. "avg"] then 384 | return 385 | end 386 | local mainhandEquipLoc, offhandEquipLoc 387 | local items = {} 388 | for slot = INVSLOT_FIRST_EQUIPPED, INVSLOT_LAST_EQUIPPED do 389 | -- shirt and tabard don't count 390 | if slot ~= INVSLOT_BODY and slot ~= INVSLOT_TABARD then 391 | local itemID = GetInventoryItemID(unit, slot) 392 | local itemLink = GetInventoryItemLink(unit, slot) 393 | if itemLink or itemID then 394 | local item = itemLink and Item:CreateFromItemLink(itemLink) or Item:CreateFromItemID(itemID) 395 | continuableContainer:AddContinuable(item) 396 | table.insert(items, item) 397 | -- slot bookkeeping 398 | local equipLoc = select(4, C_Item.GetItemInfoInstant(itemLink or itemID)) 399 | if slot == INVSLOT_MAINHAND then mainhandEquipLoc = equipLoc end 400 | if slot == INVSLOT_OFFHAND then offhandEquipLoc = equipLoc end 401 | end 402 | end 403 | end 404 | local numSlots 405 | if mainhandEquipLoc and offhandEquipLoc then 406 | numSlots = 16 407 | else 408 | local isFuryWarrior = select(2, UnitClass(unit)) == "WARRIOR" 409 | if unit == "player" then 410 | isFuryWarrior = isFuryWarrior and IsSpellKnown(46917) -- knows titan's grip 411 | else 412 | isFuryWarrior = isFuryWarrior and _G.GetInspectSpecialization and GetInspectSpecialization(unit) == 72 413 | end 414 | -- unit is holding a one-handed weapon, a main-handed weapon, or a 2h weapon while Fury: 16 slots 415 | -- otherwise 15 slots 416 | local equippedLocation = mainhandEquipLoc or offhandEquipLoc 417 | numSlots = ( 418 | equippedLocation == "INVTYPE_WEAPON" or 419 | equippedLocation == "INVTYPE_WEAPONMAINHAND" or 420 | (equippedLocation == "INVTYPE_2HWEAPON" and isFuryWarrior) 421 | ) and 16 or 15 422 | end 423 | if pcall(GetInventorySlotInfo, "RANGEDSLOT") then 424 | -- ranged slot exists until Pandaria 425 | -- C_PaperDollInfo.IsRangedSlotShown(), but that doesn't actually exist in classic... 426 | numSlots = numSlots + 1 427 | end 428 | -- if UnitHasRelicSlot("target") then 429 | -- numSlots = numSlots + 1 430 | -- end 431 | continuableContainer:ContinueOnLoad(function() 432 | local totalLevel = 0 433 | for _, item in ipairs(items) do 434 | totalLevel = totalLevel + item:GetCurrentItemLevel() 435 | end 436 | fontstring:SetFormattedText(ITEM_LEVEL, totalLevel / numSlots) 437 | fontstring:Show() 438 | end) 439 | end 440 | 441 | -- Character frame: 442 | 443 | local function UpdateItemSlotButton(button, unit) 444 | CleanButton(button) 445 | local key = unit == "player" and "character" or "inspect" 446 | if not db[key] then 447 | return 448 | end 449 | local slotID = button:GetID() 450 | 451 | if (slotID >= INVSLOT_FIRST_EQUIPPED and slotID <= INVSLOT_LAST_EQUIPPED) then 452 | local item 453 | if unit == "player" then 454 | item = Item:CreateFromEquipmentSlot(slotID) 455 | else 456 | local itemID = GetInventoryItemID(unit, slotID) 457 | local itemLink = GetInventoryItemLink(unit, slotID) 458 | if itemLink or itemID then 459 | item = itemLink and Item:CreateFromItemLink(itemLink) or Item:CreateFromItemID(itemID) 460 | end 461 | end 462 | UpdateButtonFromItem(button, item, key) 463 | return item 464 | end 465 | end 466 | 467 | do 468 | local levelUpdater = CreateFrame("Frame") 469 | levelUpdater:SetScript("OnUpdate", function(self) 470 | if not self.avglevel then 471 | if _G.CharacterModelFrame then 472 | self.avglevel = CharacterModelFrame:CreateFontString(nil, "OVERLAY") 473 | self.avglevel:SetPoint("BOTTOMLEFT", 5, 35) 474 | elseif _G.CharacterModelScene then 475 | self.avglevel = CharacterModelScene:CreateFontString(nil, "OVERLAY") 476 | self.avglevel:SetPoint("BOTTOM", 0, 20) 477 | end 478 | if self.avglevel then 479 | self.avglevel:SetFontObject(NumberFontNormal) -- GameFontHighlightSmall isn't bad 480 | end 481 | end 482 | AddAverageLevelToFontString("player", self.avglevel) 483 | self:Hide() 484 | end) 485 | levelUpdater:Hide() 486 | 487 | hooksecurefunc("PaperDollItemSlotButton_Update", function(button) 488 | UpdateItemSlotButton(button, "player") 489 | levelUpdater:Show() 490 | end) 491 | end 492 | 493 | -- and the inspect frame 494 | ns:RegisterAddonHook("Blizzard_InspectUI", function() 495 | local refresh = CreateFrame("Frame") 496 | refresh.elapsed = 0 497 | refresh:SetScript("OnUpdate", function(self, elapsed) 498 | self.elapsed = self.elapsed + elapsed 499 | if self.elapsed > 1.5 then 500 | self.elapsed = 0 501 | self:Hide() 502 | if InspectFrame.unit then 503 | -- Classic Era Anniversary specifically seems to trigger this with timings that cause an error here 504 | InspectPaperDollFrame_UpdateButtons() 505 | end 506 | end 507 | end) 508 | 509 | hooksecurefunc("InspectPaperDollItemSlotButton_Update", function(button) 510 | local item = UpdateItemSlotButton(button, InspectFrame.unit or "target") 511 | if item and item.itemID then 512 | -- the data was incompletely available, so queue a repeat 513 | refresh:Show() 514 | end 515 | -- print("updating button", button:GetName(), item and not item.itemLink and "incomplete" or item.itemLink or "X") 516 | end) 517 | local avglevel 518 | hooksecurefunc("InspectPaperDollFrame_UpdateButtons", function() 519 | if not avglevel then 520 | avglevel = InspectModelFrame:CreateFontString(nil, "OVERLAY") 521 | avglevel:SetFontObject(NumberFontNormal) 522 | -- Classic has a different frame structure until Mists: 523 | avglevel:SetPoint("BOTTOM", 0, (isClassic and LE_EXPANSION_LEVEL_CURRENT < LE_EXPANSION_MISTS_OF_PANDARIA) and 0 or 20) 524 | end 525 | AddAverageLevelToFontString(InspectFrame.unit or "target", avglevel) 526 | end) 527 | end) 528 | 529 | -- Equipment flyout in character frame 530 | 531 | if _G.EquipmentFlyout_DisplayButton then 532 | local function ItemFromEquipmentFlyoutDisplayButton(button) 533 | local flyoutSettings = EquipmentFlyoutFrame.button:GetParent().flyoutSettings 534 | if flyoutSettings.useItemLocation then 535 | local itemLocation = button:GetItemLocation() 536 | if itemLocation then 537 | return Item:CreateFromItemLocation(itemLocation) 538 | end 539 | else 540 | local location = button.location 541 | if not location then return end 542 | if location >= EQUIPMENTFLYOUT_FIRST_SPECIAL_LOCATION then return end 543 | if EquipmentManager_GetLocationData then 544 | -- 11.2.0 545 | local locationData = EquipmentManager_GetLocationData(location) 546 | if locationData.isBags then 547 | return Item:CreateFromBagAndSlot(locationData.bag, locationData.slot) 548 | end 549 | if locationData.isPlayer then 550 | return Item:CreateFromEquipmentSlot(locationData.slot) 551 | end 552 | else 553 | local player, bank, bags, voidStorage, slot, bag = EquipmentManager_UnpackLocation(location) 554 | if type(voidStorage) ~= "boolean" then 555 | -- classic compatibility: no voidStorage returns, so shuffle everything down by one 556 | -- returns either `player, bank, bags (true), slot, bag` or `player, bank, bags (false), location` 557 | slot, bag = voidStorage, slot 558 | end 559 | if bags then 560 | return Item:CreateFromBagAndSlot(bag, slot) 561 | end 562 | if not voidStorage then -- player or bank 563 | return Item:CreateFromEquipmentSlot(slot) 564 | end 565 | end 566 | local itemID = EquipmentManager_GetItemInfoByLocation(location) 567 | if itemID then 568 | -- print("fell back to itemid", location) 569 | return Item:CreateFromItemID(itemID) 570 | end 571 | end 572 | end 573 | hooksecurefunc("EquipmentFlyout_UpdateItems", function() 574 | local flyoutSettings = EquipmentFlyoutFrame.button:GetParent().flyoutSettings 575 | for i, button in ipairs(EquipmentFlyoutFrame.buttons) do 576 | CleanButton(button) 577 | if db.flyout and button:IsShown() then 578 | local item = ItemFromEquipmentFlyoutDisplayButton(button) 579 | if item then 580 | UpdateButtonFromItem(button, item, "character") 581 | end 582 | end 583 | end 584 | end) 585 | end 586 | 587 | -- Bags: 588 | 589 | local function UpdateContainerButton(button, bag, slot) 590 | CleanButton(button) 591 | if not db.bags then 592 | return 593 | end 594 | slot = slot or button:GetID() 595 | if not (bag and slot) then 596 | return 597 | end 598 | local item = Item:CreateFromBagAndSlot(bag, slot or button:GetID()) 599 | UpdateButtonFromItem(button, item, "bags") 600 | end 601 | 602 | if _G.ContainerFrame_Update then 603 | hooksecurefunc("ContainerFrame_Update", function(container) 604 | local bag = container:GetID() 605 | local name = container:GetName() 606 | for i = 1, container.size, 1 do 607 | local button = _G[name .. "Item" .. i] 608 | UpdateContainerButton(button, bag) 609 | end 610 | end) 611 | else 612 | local update = function(frame) 613 | for _, itemButton in frame:EnumerateValidItems() do 614 | UpdateContainerButton(itemButton, itemButton:GetBagID(), itemButton:GetID()) 615 | end 616 | end 617 | -- can't use ContainerFrameUtil_EnumerateContainerFrames because it depends on the combined bags setting 618 | hooksecurefunc(ContainerFrameCombinedBags, "UpdateItems", update) 619 | for _, frame in ipairs((ContainerFrameContainer or UIParent).ContainerFrames) do 620 | hooksecurefunc(frame, "UpdateItems", update) 621 | end 622 | end 623 | 624 | -- Main bank frame, bankbags are covered by containerframe above 625 | if _G.BankFrameItemButton_Update then 626 | -- pre-11.2.0 bank 627 | hooksecurefunc("BankFrameItemButton_Update", function(button) 628 | if not button.isBag then 629 | UpdateContainerButton(button, button:GetParent():GetID()) 630 | end 631 | end) 632 | end 633 | 634 | do 635 | local function hookBankPanel(panel) 636 | if not panel then return end 637 | local update = function(frame) 638 | for itemButton in frame:EnumerateValidItems() do 639 | UpdateContainerButton(itemButton, itemButton:GetBankTabID(), itemButton:GetContainerSlotID()) 640 | end 641 | end 642 | -- Initial load and switching tabs 643 | hooksecurefunc(panel, "GenerateItemSlotsForSelectedTab", update) 644 | -- Moving items 645 | hooksecurefunc(panel, "RefreshAllItemsForSelectedTab", update) 646 | end 647 | hookBankPanel(_G.BankPanel) -- added in 11.2.0 648 | hookBankPanel(_G.AccountBankPanel) -- removed in 11.2.0 649 | end 650 | 651 | -- Loot 652 | 653 | if _G.LootFrame_UpdateButton then 654 | -- Classic 655 | hooksecurefunc("LootFrame_UpdateButton", function(index) 656 | local button = _G["LootButton"..index] 657 | if not button then return end 658 | CleanButton(button) 659 | if not db.loot then return end 660 | -- ns.Debug("LootFrame_UpdateButton", button:IsEnabled(), button.slot, button.slot and GetLootSlotLink(button.slot)) 661 | if button:IsEnabled() and button.slot then 662 | local link = GetLootSlotLink(button.slot) 663 | if link then 664 | UpdateButtonFromItem(button, Item:CreateFromItemLink(link), "loot") 665 | end 666 | end 667 | end) 668 | else 669 | -- Dragonflight 670 | local ITEM_LEVEL_PATTERN = ITEM_LEVEL:gsub("%%d", "(%%d+)") 671 | local nope = {lines={}} 672 | local lineType = Enum.TooltipDataLineType.ItemLevel or Enum.TooltipDataLineType.None 673 | local function itemLevelFromLootTooltip(slot) 674 | -- GetLootSlotLink doesn't give a link for the scaled item you'll 675 | -- actually loot. As such, we can fall back on tooltip scanning to 676 | -- extract the real level. This is only going to work on 677 | -- weapons/armor, but conveniently that's the things that get scaled! 678 | if not _G.C_TooltipInfo then return end -- in case we get a weird Classic update... 679 | local info = C_TooltipInfo.GetLootItem(slot) or nope 680 | for _, line in ipairs(info.lines) do 681 | if line.type == lineType then 682 | local levelMatch = line.leftText:match(ITEM_LEVEL_PATTERN) 683 | if levelMatch then 684 | return tonumber(levelMatch) 685 | end 686 | end 687 | end 688 | end 689 | 690 | local function handleSlot(frame) 691 | if not frame.Item then return end 692 | CleanButton(frame.Item) 693 | if not db.loot then return end 694 | local data = frame:GetElementData() 695 | if not (data and data.slotIndex) then return end 696 | local link = GetLootSlotLink(data.slotIndex) 697 | if link then 698 | UpdateButtonFromItem(frame.Item, Item:CreateFromItemLink(link), "loot", nil, { 699 | level = itemLevelFromLootTooltip(data.slotIndex), 700 | }) 701 | end 702 | end 703 | LootFrame.ScrollBox:RegisterCallback("OnUpdate", function(...) 704 | LootFrame.ScrollBox:ForEachFrame(handleSlot) 705 | end) 706 | end 707 | 708 | -- Tooltip 709 | 710 | local OnTooltipSetItem = function(self) 711 | if not db.tooltip then return end 712 | local item 713 | if self.GetItem then 714 | local _, itemLink = self:GetItem() 715 | if not itemLink then return end 716 | item = Item:CreateFromItemLink(itemLink) 717 | elseif self.GetPrimaryTooltipData then 718 | local data = self:GetPrimaryTooltipData() 719 | if data and data.guid and data.type == Enum.TooltipDataType.Item then 720 | item = Item:CreateFromItemGUID(data.guid) 721 | end 722 | end 723 | if not item or item:IsItemEmpty() then return end 724 | item:ContinueOnItemLoad(function() 725 | self:AddLine(ITEM_LEVEL:format(item:GetCurrentItemLevel())) 726 | end) 727 | end 728 | if _G.C_TooltipInfo then 729 | -- Cata-classic has TooltipDataProcessor, but doesn't actually use the new tooltips 730 | TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, OnTooltipSetItem) 731 | else 732 | GameTooltip:HookScript("OnTooltipSetItem", OnTooltipSetItem) 733 | ItemRefTooltip:HookScript("OnTooltipSetItem", OnTooltipSetItem) 734 | -- This is mostly world quest rewards: 735 | if GameTooltip.ItemTooltip then 736 | GameTooltip.ItemTooltip.Tooltip:HookScript("OnTooltipSetItem", OnTooltipSetItem) 737 | end 738 | end 739 | 740 | -- Void Storage 741 | 742 | ns:RegisterAddonHook("Blizzard_VoidStorageUI", function() 743 | local VOID_STORAGE_MAX = 80 744 | hooksecurefunc("VoidStorage_ItemsUpdate", function(doStorage, doContents) 745 | if not doContents then return end 746 | for i = 1, VOID_STORAGE_MAX do 747 | local itemID, textureName, locked, recentDeposit, isFiltered, quality = GetVoidItemInfo(VoidStorageFrame.page, i) 748 | local button = _G["VoidStorageStorageButton"..i] 749 | CleanButton(button) 750 | if itemID and db.bags then 751 | local link = GetVoidItemHyperlinkString(((VoidStorageFrame.page - 1) * VOID_STORAGE_MAX) + i) 752 | if link then 753 | local item = Item:CreateFromItemLink(link) 754 | UpdateButtonFromItem(button, item, "bags") 755 | end 756 | end 757 | end 758 | end) 759 | end) 760 | 761 | -- Guild Bank 762 | 763 | ns:RegisterAddonHook("Blizzard_GuildBankUI", function() 764 | hooksecurefunc(GuildBankFrame, "Update", function(self) 765 | if self.mode ~= "bank" then return end 766 | local tab = GetCurrentGuildBankTab() 767 | for _, column in ipairs(self.Columns) do 768 | for _, button in ipairs(column.Buttons) do 769 | CleanButton(button) 770 | local link = GetGuildBankItemLink(tab, button:GetID()) 771 | if link then 772 | local item = Item:CreateFromItemLink(link) 773 | UpdateButtonFromItem(button, item, "bags") 774 | end 775 | end 776 | end 777 | end) 778 | end) 779 | 780 | -- Inventorian 781 | ns:RegisterAddonHook("Inventorian", function() 782 | local inv = LibStub("AceAddon-3.0", true):GetAddon("Inventorian", true) 783 | local function ToIndex(bag, slot) -- copied from inside Inventorian 784 | return (bag < 0 and bag * 100 - slot) or (bag * 100 + slot) 785 | end 786 | local function invContainerUpdateSlot(self, bag, slot) 787 | local button = self.items[ToIndex(bag, slot)] 788 | if not button then return end 789 | if button:IsCached() then 790 | local item 791 | local icon, count, locked, quality, readable, lootable, link, noValue, itemID, isBound = button:GetInfo() 792 | if link then 793 | item = Item:CreateFromItemLink(link) 794 | elseif itemID then 795 | item = Item:CreateFromItemID(itemID) 796 | end 797 | CleanButton(button) 798 | UpdateButtonFromItem(button, item, "bags") 799 | else 800 | UpdateContainerButton(button, bag, slot) 801 | end 802 | end 803 | local function hookInventorian() 804 | hooksecurefunc(inv.bag.itemContainer, "UpdateSlot", invContainerUpdateSlot) 805 | hooksecurefunc(inv.bank.itemContainer, "UpdateSlot", invContainerUpdateSlot) 806 | end 807 | if inv.bag then 808 | hookInventorian() 809 | else 810 | hooksecurefunc(inv, "OnEnable", function() 811 | hookInventorian() 812 | end) 813 | end 814 | end) 815 | 816 | --Baggins: 817 | ns:RegisterAddonHook("Baggins", function() 818 | hooksecurefunc(Baggins, "UpdateItemButton", function(baggins, bagframe, button, bag, slot) 819 | UpdateContainerButton(button, bag) 820 | end) 821 | end) 822 | 823 | --Bagnon: 824 | do 825 | local function bagbrother_button(button) 826 | CleanButton(button) 827 | if not db.bags then 828 | return 829 | end 830 | local bag = button:GetBag() 831 | if type(bag) ~= "number" or button:GetClassName() ~= "BagnonContainerItem" then 832 | local info = button:GetInfo() 833 | if info and info.hyperlink then 834 | local item = Item:CreateFromItemLink(info.hyperlink) 835 | UpdateButtonFromItem(button, item, "bags") 836 | end 837 | return 838 | end 839 | UpdateContainerButton(button, bag) 840 | end 841 | ns:RegisterAddonHook("Bagnon", function() 842 | hooksecurefunc(Bagnon.Item, "Update", bagbrother_button) 843 | end) 844 | 845 | --Bagnonium (exactly same internals as Bagnon): 846 | ns:RegisterAddonHook("Bagnonium", function() 847 | hooksecurefunc(Bagnonium.Item, "Update", bagbrother_button) 848 | end) 849 | 850 | --Combuctor (exactly same internals as Bagnon): 851 | ns:RegisterAddonHook("Combuctor", function() 852 | hooksecurefunc(Combuctor.Item, "Update", bagbrother_button) 853 | end) 854 | end 855 | 856 | --LiteBag: 857 | ns:RegisterAddonHook("LiteBag", function() 858 | _G.LiteBag_RegisterHook('LiteBagItemButton_Update', function(frame) 859 | local bag = frame:GetParent():GetID() 860 | UpdateContainerButton(frame, bag) 861 | end) 862 | end) 863 | 864 | -- Baganator 865 | ns:RegisterAddonHook("Baganator", function() 866 | local function textInit(itemButton) 867 | local text = itemButton:CreateFontString(nil, "OVERLAY", "NumberFontNormal") 868 | text.sizeFont = true 869 | return text 870 | end 871 | -- Note to self: Baganator API update function returns are tri-state: 872 | -- true: something to show 873 | -- false: nothing to show 874 | -- nil: call this again soon (probably because of item-caching) 875 | local function onUpdate(callback) 876 | return function(cornerFrame, details) 877 | if not details.itemLink then return false end 878 | local button = cornerFrame:GetParent():GetParent() 879 | local item 880 | -- If we have a container-item, we should use that because it's needed for soulbound detection 881 | local bag, slot = button:GetParent():GetID(), button:GetID() 882 | -- print("SetItemDetails", details.itemLink, bag, slot) 883 | if bag and slot and slot ~= 0 then 884 | item = Item:CreateFromBagAndSlot(bag, slot) 885 | elseif details.itemLink then 886 | item = Item:CreateFromItemLink(details.itemLink) 887 | end 888 | if not item then return false end -- no item, go away 889 | if not item:IsItemDataCached() then return nil end -- item isn't cached, come back in a second 890 | local data = DetailsFromItem(item) 891 | return callback(cornerFrame, item, data, details) 892 | end 893 | end 894 | Baganator.API.RegisterCornerWidget("sIlvl: Item Level", "simpleitemlevel-ilvl", 895 | onUpdate(function(cornerFrame, item, data, details) 896 | cornerFrame:SetText(data.level) 897 | if db.color and data.quality then 898 | local r, g, b = C_Item.GetItemQualityColor(data.quality) 899 | cornerFrame:SetTextColor(r, g, b) 900 | else 901 | cornerFrame:SetTextColor(1, 1, 1) 902 | end 903 | return true 904 | end), 905 | textInit, {default_position = "top_right", priority = 1} 906 | ) 907 | Baganator.API.RegisterCornerWidget("sIlvl: Upgrade", "simpleitemlevel-upgrade", 908 | onUpdate(function(cornerFrame, item, data, details) 909 | if data.upgrade then 910 | local minLevel = select(5, C_Item.GetItemInfo(item:GetItemLink() or item:GetItemID())) 911 | if minLevel and minLevel > UnitLevel("player") then 912 | cornerFrame:SetVertexColor(1, 0, 0) 913 | else 914 | cornerFrame:SetVertexColor(1, 1, 1) 915 | end 916 | return true 917 | end 918 | return false 919 | end), 920 | function (itemButton) 921 | local texture = itemButton:CreateTexture(nil, "ARTWORK") 922 | texture:SetAtlas(ns.upgradeAtlas) 923 | texture:SetSize(11, 11) 924 | return texture 925 | end, 926 | {default_position = "top_left", priority = 1} 927 | ) 928 | Baganator.API.RegisterCornerWidget("sIlvl: Soulbound", "simpleitemlevel-bound", 929 | onUpdate(function(cornerFrame, item, data, details) 930 | return ColorFrameByBinding(cornerFrame, data) 931 | end), 932 | function (itemButton) 933 | local texture = itemButton:CreateTexture(nil, "ARTWORK") 934 | texture:SetAtlas(ns.soulboundAtlas) 935 | texture:SetSize(12, 12) 936 | return texture 937 | end, {default_position = "bottom_left", priority = 1} 938 | ) 939 | Baganator.API.RegisterCornerWidget("sIlvl: Missing", "simpleitemlevel-missing", 940 | onUpdate(function(cornerFrame, item, data, details) 941 | if db.missingcharacter then return false end 942 | local missingGems = db.missinggems and data.missingGems 943 | local missingEnchants = db.missingenchants and data.missingEnchants 944 | if missingGems or missingEnchants then 945 | cornerFrame:SetFormattedText("%s%s", missingGems and ns.gemString or "", missingEnchants and ns.enchantString or "") 946 | return true 947 | end 948 | return false 949 | end), 950 | textInit, {default_position = "bottom_right", priority = 2} 951 | ) 952 | end) 953 | 954 | -- helper 955 | 956 | do 957 | local EquipLocToSlot1 = { 958 | INVTYPE_HEAD = 1, 959 | INVTYPE_NECK = 2, 960 | INVTYPE_SHOULDER = 3, 961 | INVTYPE_BODY = 4, 962 | INVTYPE_CHEST = 5, 963 | INVTYPE_ROBE = 5, 964 | INVTYPE_WAIST = 6, 965 | INVTYPE_LEGS = 7, 966 | INVTYPE_FEET = 8, 967 | INVTYPE_WRIST = 9, 968 | INVTYPE_HAND = 10, 969 | INVTYPE_FINGER = 11, 970 | INVTYPE_TRINKET = 13, 971 | INVTYPE_CLOAK = 15, 972 | INVTYPE_WEAPON = 16, 973 | INVTYPE_SHIELD = 17, 974 | INVTYPE_2HWEAPON = 16, 975 | INVTYPE_WEAPONMAINHAND = 16, 976 | INVTYPE_RANGED = 16, 977 | INVTYPE_RANGEDRIGHT = 16, 978 | INVTYPE_WEAPONOFFHAND = 17, 979 | INVTYPE_HOLDABLE = 17, 980 | INVTYPE_TABARD = 19, 981 | } 982 | local EquipLocToSlot2 = { 983 | INVTYPE_FINGER = 12, 984 | INVTYPE_TRINKET = 14, 985 | INVTYPE_WEAPON = 17, 986 | } 987 | local ForEquippedItem = function(slot, callback) 988 | if not slot then 989 | return 990 | end 991 | local item = Item:CreateFromEquipmentSlot(slot) 992 | return callback(item, slot) 993 | end 994 | ns.ForEquippedItems = function(equipLoc, callback) 995 | ForEquippedItem(EquipLocToSlot1[equipLoc], callback) 996 | ForEquippedItem(EquipLocToSlot2[equipLoc], callback) 997 | end 998 | end 999 | 1000 | ns.CacheEquippedItems = function() 1001 | for slotID = INVSLOT_FIRST_EQUIPPED, INVSLOT_LAST_EQUIPPED do 1002 | local itemID = GetInventoryItemID("player", slotID) 1003 | if itemID then 1004 | C_Item.RequestLoadItemDataByID(itemID) 1005 | end 1006 | end 1007 | end 1008 | 1009 | do 1010 | -- could arguably also do TooltipDataProcessor.AddLinePostCall(Enum.TooltipDataLineType.GemSocket, ...) 1011 | local GetItemStats = C_Item and C_Item.GetItemStats or _G.GetItemStats 1012 | function ns.ItemHasEmptySlots(itemLink) 1013 | if not itemLink then return end 1014 | local stats = GetItemStats(itemLink) 1015 | if not stats then return false end -- caged battle pets, mostly 1016 | local slots = 0 1017 | for label, stat in pairs(stats) do 1018 | if label:match("EMPTY_SOCKET_") then 1019 | slots = slots + 1 1020 | end 1021 | end 1022 | if slots == 0 then return false end 1023 | local gem1, gem2, gem3, gem4 = select(4, ns.GetLinkValues(itemLink)) 1024 | local gems = (gem1 ~= "" and 1 or 0) + (gem2 ~= "" and 1 or 0) + (gem3 ~= "" and 1 or 0) + (gem4 ~= "" and 1 or 0) 1025 | return slots > gems 1026 | end 1027 | local enchantable = isClassic and { 1028 | INVTYPE_HEAD = true, 1029 | INVTYPE_SHOULDER = true, 1030 | INVTYPE_CHEST = true, 1031 | INVTYPE_ROBE = true, 1032 | INVTYPE_LEGS = true, 1033 | INVTYPE_FEET = true, 1034 | INVTYPE_WRIST = true, 1035 | INVTYPE_HAND = true, 1036 | INVTYPE_FINGER = true, 1037 | INVTYPE_CLOAK = true, 1038 | INVTYPE_WEAPON = true, 1039 | INVTYPE_SHIELD = true, 1040 | INVTYPE_2HWEAPON = true, 1041 | INVTYPE_WEAPONMAINHAND = true, 1042 | INVTYPE_RANGED = true, 1043 | INVTYPE_RANGEDRIGHT = true, 1044 | INVTYPE_WEAPONOFFHAND = true, 1045 | INVTYPE_HOLDABLE = true, 1046 | } or { 1047 | -- retail 1048 | INVTYPE_CHEST = true, 1049 | INVTYPE_ROBE = true, 1050 | INVTYPE_LEGS = true, 1051 | INVTYPE_FEET = true, 1052 | INVTYPE_WRIST = true, 1053 | INVTYPE_FINGER = true, 1054 | INVTYPE_CLOAK = true, 1055 | INVTYPE_WEAPON = true, 1056 | INVTYPE_2HWEAPON = true, 1057 | INVTYPE_WEAPONMAINHAND = true, 1058 | INVTYPE_RANGED = true, 1059 | INVTYPE_RANGEDRIGHT = true, 1060 | INVTYPE_WEAPONOFFHAND = true, 1061 | } 1062 | function ns.ItemIsMissingEnchants(itemLink) 1063 | if not itemLink then return false end 1064 | local equipLoc = select(4, C_Item.GetItemInfoInstant(itemLink)) 1065 | if not enchantable[equipLoc] then return false end 1066 | local enchantID = select(3, ns.GetLinkValues(itemLink)) 1067 | if enchantID == "" then return true end 1068 | return false 1069 | end 1070 | end 1071 | 1072 | ns.GetLinkValues = function(link) 1073 | local linkType, linkOptions, displayText = LinkUtil.ExtractLink(link) 1074 | return linkType, strsplit(":", linkOptions) 1075 | end 1076 | --------------------------------------------------------------------------------