diff Junk.lua @ 161:35612aee8e15

Added junk list.
author yellowfive
date Mon, 06 May 2019 14:08:03 -0700
parents
children f66108c1f674
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Junk.lua	Mon May 06 14:08:03 2019 -0700
@@ -0,0 +1,656 @@
+local Amr = LibStub("AceAddon-3.0"):GetAddon("AskMrRobot")
+local L = LibStub("AceLocale-3.0"):GetLocale("AskMrRobot", true)
+local AceGUI = LibStub("AceGUI-3.0")
+
+local _frameJunk
+local _lblAction
+local _lblBank
+local _btnBank
+local _panelContent
+
+local _canDisenchant = false
+local _isScrapOpen = false
+local _isMerchantOpen = false
+local _isBankOpen = false
+
+--
+-- Scan a bag for the specified item, returning the first exact match found
+--
+local function scanBag(bagId, matchItem, usedItems)
+
+	local numSlots = GetContainerNumSlots(bagId)
+    local loc = ItemLocation.CreateEmpty()
+    
+    if not usedItems[bagId] then
+        usedItems[bagId] = {}
+    end
+
+    for slotId = 1, numSlots do
+        if not usedItems[bagId][slotId] then
+            local _, itemCount, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId)
+            if itemLink ~= nil then
+                local itemData = Amr.Serializer.ParseItemLink(itemLink)
+                if itemData ~= nil then
+                    -- see if this is an azerite item and read azerite power ids
+                    loc:SetBagAndSlot(bagId, slotId)
+                    if C_AzeriteEmpoweredItem.IsAzeriteEmpoweredItem(loc) then
+                        local powers = Amr.ReadAzeritePowers(loc)
+                        if powers then
+                            itemData.azerite = powers
+                        end
+                    end
+                    
+                    -- see if it matches
+                    if Amr.CountItemDifferences(matchItem, itemData) == 0 then
+                        usedItems[bagId][slotId] = true
+                        itemData.bagId = bagId
+                        itemData.slotId = slotId
+                        return itemData
+                    end
+                end
+            end
+        end
+    end
+    
+    return nil
+end
+
+--
+-- Find a matching item in the player's bags
+--
+local function findMatchingBagItem(item, usedItems)
+
+    local matchItem = scanBag(BACKPACK_CONTAINER, item, usedItems) -- backpack
+    if not matchItem then
+        for bagId = 1, NUM_BAG_SLOTS do
+            matchItem = scanBag(bagId, item, usedItems)
+            if matchItem then break end
+        end
+    end
+    
+    return matchItem
+end
+
+
+--
+-- item actions
+--
+local _deSpellName = GetSpellInfo(13262);
+local _deMacro = "/stopmacro [combat][btn:2]\n/stopcasting\n/cast %s\n/use %s %s";
+
+local function onItemPreClick(widget)
+
+    local item = widget:GetUserData("itemData")
+    
+    if item and _canDisenchant and (not _isScrapOpen and not _isMerchantOpen) then
+        -- only way i can find to disenchant and item on click is to call a macro, gross but works
+        local matchItem = findMatchingBagItem(item, {})
+        if matchItem then
+            widget:SetMacroText(_deMacro:format(_deSpellName, matchItem.bagId, matchItem.slotId))
+        else
+            widget:SetMacroText(nil)
+            Amr:Print(L.JunkItemNotFound)
+        end
+    else
+        widget:SetMacroText(nil)
+    end
+end
+
+local function onItemClick(widget)
+    
+    local item = widget:GetUserData("itemData")
+    if not item then return end
+
+    local action = nil
+    if _isScrapOpen then
+        action = "scrap"
+    elseif _isMerchantOpen then
+        action = "sell"
+    elseif _canDisenchant then
+        action = "disenchant"
+    end
+
+    if not action then return end
+
+    local matchItem = findMatchingBagItem(item, {})
+    if matchItem then
+        if action == "scrap" then
+            UseContainerItem(matchItem.bagId, matchItem.slotId)
+        elseif action == "sell" then
+            UseContainerItem(matchItem.bagId, matchItem.slotId)
+        end
+
+        -- note for disenchant, the macro has handled the action, this will simply remove the item from the list
+
+        -- re-render the list with this item removed;
+        -- AceGUI doesn't give a good way to remove a ui element from a container and re-render, 
+        -- so we sort of hack it and modify the collection of children directly, 
+        -- avoids the expensive logic of finding and matching all the items when the list changes as a user sells stuff
+        local scroll = widget.parent.parent
+        local newChildren = {}
+        for i = 1, #scroll.children do
+            local child = scroll.children[i]
+            if child ~= widget.parent then
+                table.insert(newChildren, child)
+            end
+        end
+        scroll.children = newChildren
+
+        -- dispose the item just removed, then re-render the list
+        widget.parent:Release()
+        widget.parent.parent:DoLayout()
+    else
+        Amr:Print(L.JunkItemNotFound)
+    end
+end
+
+
+--
+-- bank withdraw stuff
+--
+local _bankUsedBagSlots = nil
+local finishBankWithdraw
+local doBankWithdraw
+
+finishBankWithdraw = function()
+
+    local done = true
+
+    if _isBankOpen and _bankUsedBagSlots then
+        for bagId,v in pairs(_bankUsedBagSlots) do
+            for slotId,v in pairs(v) do
+                local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId)
+                if not itemLink then
+                    done = false
+                    break
+                end
+            end
+            if not done then break end
+        end
+    end
+
+    if not done then
+        -- wait a second and try again
+        Amr.Wait(1, function()
+            doBankWithdraw()
+        end)
+    else
+
+        print("bank withdraw done")
+
+        -- reset state
+        _bankUsedBagSlots = nil
+        _btnBank:SetDisabled(not _isBankOpen)
+
+        -- re-render the junk list
+        Amr:RefreshJunkUi()
+    end
+end
+
+doBankWithdraw = function()
+    if not _isBankOpen then return end
+
+    local data = Amr.db.char.JunkData
+    if not data.Junk then return end
+    
+    print("doing bank withdraw...")
+
+    -- disable button while processing
+    _btnBank:SetDisabled(true)
+
+    local bagList = {}
+	table.insert(bagList, BANK_CONTAINER)
+	for bagId = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
+		table.insert(bagList, bagId)
+	end
+
+    local usedItems = {}
+    _bankUsedBagSlots = {}
+
+    for i,item in ipairs(data.Junk) do
+        -- stop immediately if the bank is closed
+        if not _isBankOpen then 
+            finishBankWithdraw()
+            return
+        end
+
+        -- check if already in bags
+        local matchItem = findMatchingBagItem(item, usedItems)
+        if not matchItem then
+            -- find it in the bank
+            for j = 1, #bagList do
+                matchItem = scanBag(bagList[j], item, usedItems)
+                if matchItem then break end
+            end
+        else
+            matchItem = nil
+        end
+
+        if matchItem then            
+            -- move it to the player's bags if there is space
+            local bagId, slotId = Amr.FindFirstEmptyBagSlot(_bankUsedBagSlots)
+            if bagId then
+                UseContainerItem(matchItem.bagId, matchItem.slotId)
+            else
+                -- no more empty bag slots
+                break
+            end
+        end
+    end
+
+    -- wait a second and see if all the moves actually finished
+    Amr.Wait(1, function()
+        finishBankWithdraw()
+    end)
+end
+
+local function onBankClick()
+    if not _frameJunk or not _isBankOpen then return end
+
+	doBankWithdraw()
+end
+
+
+local function onJunkFrameClose(widget)
+	AceGUI:Release(widget)
+    _frameJunk = nil
+    _lblAction = nil
+    _lblBank = nil
+    _btnBank = nil
+    _panelContent = nil
+end
+
+function Amr:HideJunkWindow()
+	if not _frameJunk then return end
+	_frameJunk:Hide()
+end
+
+function Amr:ShowJunkWindow()
+
+	if not _frameJunk then
+		_frameJunk = AceGUI:Create("AmrUiFrame")
+		_frameJunk:SetStatusTable(Amr.db.profile.junkWindow) -- window position is remembered in db
+		_frameJunk:SetCallback("OnClose", onJunkFrameClose)
+		_frameJunk:SetLayout("None")
+		_frameJunk:SetWidth(400)
+		_frameJunk:SetHeight(700)
+		_frameJunk:SetBorderColor(Amr.Colors.BorderBlue)
+		_frameJunk:SetBackgroundColor(Amr.Colors.Bg)
+		
+		if Amr.db.profile.options.uiScale ~= 1 then
+			local scale = tonumber(Amr.db.profile.options.uiScale)
+			_frameJunk:SetScale(scale)
+		end
+		
+		local lbl = AceGUI:Create("AmrUiLabel")
+		_frameJunk:AddChild(lbl)
+		lbl:SetWidth(300)
+		lbl:SetFont(Amr.CreateFont("Bold", 28, Amr.Colors.White))
+		lbl:SetText(L.JunkTitle)
+		lbl:SetWordWrap(false)
+		lbl:SetJustifyH("CENTER")
+		lbl:SetPoint("TOP", _frameJunk.content, "TOP", 0, 30)		
+		lbl:SetCallback("OnMouseDown", function(widget) _frameJunk:StartMove() end)
+		lbl:SetCallback("OnMouseUp", function(widget) _frameJunk:EndMove() end)
+        
+        _lblAction = AceGUI:Create("AmrUiLabel")
+        _frameJunk:AddChild(_lblAction)
+        _lblAction:SetWidth(380)
+        _lblAction:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.TextTan))
+        _lblAction:SetText(" ")
+        _lblAction:SetWordWrap(false)
+        _lblAction:SetPoint("TOPLEFT", _frameJunk.content, "TOPLEFT", 0, -10)
+
+        _btnBank = AceGUI:Create("AmrUiButton")
+        _frameJunk:AddChild(_btnBank)
+        _btnBank:SetText(L.JunkButtonBank)
+        _btnBank:SetBackgroundColor(Amr.Colors.Green)
+        _btnBank:SetFont(Amr.CreateFont("Bold", 14, Amr.Colors.White))
+        _btnBank:SetWidth(180)
+        _btnBank:SetHeight(26)
+        _btnBank:SetDisabled(true)
+        _btnBank:SetCallback("OnClick", onBankClick)
+        _btnBank:SetPoint("BOTTOMLEFT", _frameJunk.content, "BOTTOMLEFT")
+
+        _lblBank = AceGUI:Create("AmrUiLabel")
+        _frameJunk:AddChild(_lblBank)
+        _lblBank:SetWidth(380)
+        _lblBank:SetFont(Amr.CreateFont("Bold", 15, Amr.Colors.TextHeaderActive))
+        _lblBank:SetText(L.JunkBankText(0))
+        _lblBank:SetPoint("BOTTOMLEFT", _btnBank.frame, "TOPLEFT", 0, 10)
+
+        local line = AceGUI:Create("AmrUiPanel")
+        _frameJunk:AddChild(line)
+        line:SetHeight(1)
+        line:SetBackgroundColor(Amr.Colors.White)
+        line:SetPoint("TOPLEFT", _frameJunk.content, "TOPLEFT", 0, -30)
+        line:SetPoint("TOPRIGHT", _frameJunk.content, "TOPRIGHT", 0, -30)
+
+        line = AceGUI:Create("AmrUiPanel")
+        _frameJunk:AddChild(line)
+        line:SetHeight(1)
+        line:SetBackgroundColor(Amr.Colors.White)
+        line:SetPoint("TOPLEFT", _frameJunk.content, "BOTTOMLEFT", 0, 60)
+        line:SetPoint("TOPRIGHT", _frameJunk.content, "BOTTOMRIGHT", 0, 60)
+
+        _panelContent = AceGUI:Create("AmrUiPanel")
+		_panelContent:SetLayout("None")
+		_panelContent:SetTransparent()
+		_frameJunk:AddChild(_panelContent)
+		_panelContent:SetPoint("TOPLEFT", _frameJunk.content, "TOPLEFT", 0, -31)
+        _panelContent:SetPoint("BOTTOMRIGHT", _frameJunk.content, "BOTTOMRIGHT", 0, 60)
+        
+
+		Amr:RefreshJunkUi()
+	else
+		_frameJunk:Show()
+		Amr:RefreshJunkUi()
+	end
+	
+	_frameJunk:Raise()
+end
+
+local function canDisenchant()
+
+    local prof1, prof2 = GetProfessions();
+    local profs = {}
+    table.insert(profs, prof1)
+    table.insert(profs, prof2)
+    for i,prof in ipairs(profs) do
+        if prof then
+            local _, _, skillLevel, _, _, _, skillLine = GetProfessionInfo(prof);
+            if Amr.ProfessionSkillLineToName[skillLine] == "Enchanting" and skillLevel > 0 then
+                return true
+            end
+        end
+    end
+    
+    return false
+end
+
+--
+-- Find a matching item that is not in the player's inventory (bank or equipped)
+--
+local function findMatchingNonBagItem(matchItem, usedItems)
+
+    local loc = ItemLocation.CreateEmpty()
+    
+    -- check equipped
+    local equippedBagId = -1000
+    if not usedItems[equippedBagId] then
+        usedItems[equippedBagId] = {}
+    end
+
+	for slotNum = 1, #Amr.SlotIds do
+        local slotId = Amr.SlotIds[slotNum]
+        if not usedItems[equippedBagId][slotId] then
+            local itemLink = GetInventoryItemLink("player", slotId)
+            if itemLink then
+                local itemData = Amr.ParseItemLink(itemLink)
+                if itemData then
+                    -- see if this is an azerite item and read azerite power ids
+                    loc:SetEquipmentSlot(slotId)
+                    if C_AzeriteEmpoweredItem.IsAzeriteEmpoweredItem(loc) then
+                        local powers = Amr.ReadAzeritePowers(loc)
+                        if powers then
+                            itemData.azerite = powers
+                        end
+                    end
+
+                    -- see if it matches
+                    if Amr.CountItemDifferences(matchItem, itemData) == 0 then
+                        usedItems[equippedBagId][slotId] = true
+                        itemData.bagId = bagId
+                        itemData.slotId = slotId
+                        return itemData
+                    end
+                end
+            end
+        end
+    end
+    
+    -- check bank data
+    if Amr.db.char.BankItems then
+        for bagId, v in pairs(Amr.db.char.BankItems) do            
+            if not usedItems[bagId] then
+                usedItems[bagId] = {}
+            end
+
+            for i, itemData in ipairs(v) do
+                if itemData and not usedItems[bagId][i] then
+                    -- see if it matches
+                    if Amr.CountItemDifferences(matchItem, itemData) == 0 then
+                        usedItems[bagId][i] = true
+                        itemData.bagId = bagId
+                        itemData.slotId = i
+                        return itemData
+                    end
+                end
+            end
+        end
+    end
+
+    return nil
+end
+
+local function renderItem(item, itemLink, scroll)
+
+    local panel = AceGUI:Create("AmrUiPanel")
+	scroll:AddChild(panel)
+	panel:SetLayout("None")
+	panel:SetTransparent()
+	panel:SetWidth(380)
+	panel:SetHeight(40)
+
+    -- ilvl label
+    local lblIlvl = AceGUI:Create("AmrUiLabel")
+    panel:AddChild(lblIlvl)
+    lblIlvl:SetPoint("LEFT", panel.content, "LEFT", 0, 0) 
+    lblIlvl:SetWidth(35)
+    lblIlvl:SetFont(Amr.CreateFont("Italic", 13, Amr.Colors.TextTan))		
+
+    -- icon
+    local icon = AceGUI:Create("AmrUiIcon")
+    panel:AddChild(icon)
+    icon:SetBorderWidth(1)
+    icon:SetIconBorderColor(Amr.Colors.White)
+    icon:SetWidth(28)
+    icon:SetHeight(28)
+    icon:SetPoint("LEFT", lblIlvl.frame, "RIGHT", 0, 0)
+
+    -- item name/link label
+    local lblItem = AceGUI:Create("AmrUiTextButton")
+    panel:AddChild(lblItem)
+    lblItem:SetPoint("LEFT", icon.frame, "RIGHT", 0, 0) 
+    lblItem:SetWordWrap(false)
+    lblItem:SetJustifyH("LEFT")
+    lblItem:SetWidth(300)
+    lblItem:SetHeight(28)
+    lblItem:SetFont(Amr.CreateFont("Regular", 13, Amr.Colors.White))		
+    lblItem:SetHoverBackgroundColor(Amr.Colors.Black, 0.3)
+    lblItem:SetTextPadding(0, 0, 0, 5)
+    lblItem:SetCallback("PreClick", onItemPreClick)
+    lblItem:SetCallback("OnClick", onItemClick)
+    lblItem:SetUserData("itemData", item)
+
+    -- fill the name/ilvl labels, which may require asynchronous loading of item information			
+    if itemLink then
+        local gameItem = Item:CreateFromItemLink(itemLink)
+        if gameItem then
+            local q = gameItem:GetItemQuality()
+            lblItem:SetFont(Amr.CreateFont("Regular", 13, Amr.Colors.Qualities[q] or Amr.Colors.White))
+            lblItem:SetHoverFont(Amr.CreateFont("Regular", 13, Amr.Colors.Qualities[q] or Amr.Colors.White))
+            lblItem:SetText(gameItem:GetItemName())
+            lblIlvl:SetText(gameItem:GetCurrentItemLevel())
+            icon:SetIconBorderColor(Amr.Colors.Qualities[q] or Amr.Colors.White)
+            icon:SetIcon(gameItem:GetItemIcon())
+            Amr:SetItemTooltip(lblItem, gameItem:GetItemLink(), "ANCHOR_BOTTOMRIGHT", 0, 30)
+        end
+    end
+
+end
+
+--
+-- Just updates state without re-rendering the list of junk
+--
+function Amr:SetJunkUiState()
+
+    -- don't do anything if the window is not open
+    if not _frameJunk then return end
+
+    -- cache whether the player can disenchant whenever the ui is refreshed
+    _canDisenchant = canDisenchant()
+
+    -- update action label
+    if _isScrapOpen then
+        _lblAction:SetText(L.JunkScrap)
+    elseif _isMerchantOpen then
+        _lblAction:SetText(L.JunkVendor)
+    elseif _canDisenchant then
+        _lblAction:SetText(L.JunkDisenchant)
+    else
+        _lblAction:SetText(" ")
+    end
+
+    -- update bank button state
+    _btnBank:SetDisabled(not _isBankOpen)
+end
+
+--
+-- Refresh the entire UI, including re-rendering the junk list
+--
+function Amr:RefreshJunkUi()
+
+    -- don't do anything if the window is not open
+    if not _frameJunk then return end
+
+    Amr:SetJunkUiState()
+
+    -- clear out any previous data
+	_panelContent:ReleaseChildren()
+
+    local data = Amr.db.char.JunkData
+
+    if not data or not data.Junk or #data.Junk <= 0 then
+		local lbl = AceGUI:Create("AmrUiLabel")
+		_panelContent:AddChild(lbl)
+		lbl:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextGray))
+		lbl:SetText(L.JunkEmpty)
+        lbl:SetPoint("TOPLEFT", _panelContent.content, "TOPLEFT", 0, -10)
+        _lblBank:SetVisible(false)
+        _btnBank:SetVisible(false)
+    else
+
+        _panelContent:SetLayout("Fill")
+			
+        local scroll = AceGUI:Create("AmrUiScrollFrame")
+        scroll:SetLayout("List")
+        _panelContent:AddChild(scroll)
+
+        -- render items currently in the player's inventory
+        local usedItems = {}
+        local bankCount = 0
+        local missingCount = 0
+
+        -- if we have any "keep" items, those are exact duplicates of ones to be junked, 
+        -- be sure to "reserve" those first
+        if data.Keep then
+            for uniqueId, item in pairs(data.Keep) do
+                -- check if an exact match is in the player's bank data or equipped
+                local matchItem = findMatchingNonBagItem(item, usedItems)
+
+                -- if not, find one in the player's bags
+                if not matchItem then
+                    matchItem = findMatchingBagItem(item, usedItems)
+                end
+
+                if not matchItem then
+                    -- abort, player's data must be out of sync
+                    local lbl = AceGUI:Create("AmrUiLabel")
+                    _panelContent:AddChild(lbl)
+                    lbl:SetWidth(380)
+                    lbl:SetFont(Amr.CreateFont("Italic", 13, Amr.Colors.TextGray))
+                    lbl:SetText(L.JunkOutOfSync)
+                    lbl:SetPoint("TOPLEFT", _panelContent.content, "TOPLEFT", 0, -10)
+
+                    _lblBank:SetVisible(false)
+                    _btnBank:SetVisible(false)
+
+                    return
+                end
+            end
+        end
+
+        -- now render any junk items in the player's bags, and a count of how many are elsewhere (usually bank)
+        for i, item in ipairs(data.Junk) do
+            local matchItem = findMatchingBagItem(item, usedItems)
+            if matchItem then
+                local itemLink = Amr.CreateItemLink(matchItem)
+                renderItem(matchItem, itemLink, scroll)
+            else
+                -- see if it is in the bank or equipped
+                matchItem = findMatchingNonBagItem(item, usedItems)
+                if matchItem then
+                    bankCount = bankCount + 1
+                else
+                    missingCount = missingCount + 1
+                end
+            end
+        end
+
+        _lblBank:SetText(L.JunkBankText(bankCount))
+        _lblBank:SetVisible(bankCount > 0)
+        _btnBank:SetVisible(bankCount > 0)
+
+        -- in most cases, this is because the user has sold/scrapped the items already
+        --if missingCount > 0 then
+        --    Amr:Print(L.JunkMissingText(missingCount))
+        --end
+    end
+end
+
+Amr:AddEventHandler("SCRAPPING_MACHINE_SHOW", function() 
+	_isScrapOpen = true
+	if Amr.db.profile.options.junkVendor and Amr.db.char.JunkData and Amr.db.char.JunkData.Junk and #Amr.db.char.JunkData.Junk > 0 then
+        Amr:ShowJunkWindow()
+    else
+        Amr:SetJunkUiState()
+	end
+end)
+
+Amr:AddEventHandler("SCRAPPING_MACHINE_CLOSE", function() 
+	_isScrapOpen = false
+	if Amr.db.profile.options.junkVendor then
+        Amr:HideJunkWindow()
+    else
+        Amr:SetJunkUiState()
+	end
+end)
+
+Amr:AddEventHandler("MERCHANT_SHOW", function() 
+	_isMerchantOpen = true
+	if Amr.db.profile.options.junkVendor and Amr.db.char.JunkData and Amr.db.char.JunkData.Junk and #Amr.db.char.JunkData.Junk > 0 then
+        Amr:ShowJunkWindow()
+    else
+        Amr:SetJunkUiState()
+	end
+end)
+
+Amr:AddEventHandler("MERCHANT_CLOSED", function() 
+	_isMerchantOpen = false
+	if Amr.db.profile.options.junkVendor then
+        Amr:HideJunkWindow()
+    else
+        Amr:SetJunkUiState()
+	end
+end)
+
+Amr:AddEventHandler("BANKFRAME_OPENED", function() 
+    _isBankOpen = true
+    Amr:SetJunkUiState()
+end)
+
+Amr:AddEventHandler("BANKFRAME_CLOSED", function() 
+    _isBankOpen = false
+    Amr:SetJunkUiState()
+end)