local MAJOR,MINOR = "LibBagUtils-1.0", tonumber(("$Revision: 35 $"):match("%d+")) local lib = LibStub:NewLibrary(MAJOR,MINOR) -- -- LibBagUtils -- -- Several useful bag related APIs that you wish were built into the WoW API: -- :PutItem() -- :Iterate() -- :LinkIsItem() - which amongst other things handles the 3.2 wotlk randomstat item madness (changing while in AH/mail/gbank) -- :GetNumFreeSlots() -- .. and more! -- -- Pains have been taken to make sure to use as much FrameXML data and constants as possible, -- which should let the library (and dependant addons) keep functioning if Blizzard desides -- to add more bags, or reorder them. -- -- Read the well-commented "API" function headers for each function below for usage and descriptions. -- if not lib then return end -- no upgrade needed local strmatch=string.match local gsub=string.gsub local floor=math.floor local tconcat = table.concat local band=bit.band local pairs,select,type,next,tonumber,tostring=pairs,select,type,next,tonumber,tostring local GetTime=GetTime local GetContainerNumSlots, GetContainerNumFreeSlots = GetContainerNumSlots, GetContainerNumFreeSlots local GetContainerItemLink,GetContainerItemInfo = GetContainerItemLink,GetContainerItemInfo local GetItemInfo, GetItemFamily = GetItemInfo, GetItemFamily -- GLOBALS: error, geterrorhandler, PickupContainerItem -- GLOBALS: CursorHasItem, ClearCursor, GetCursorInfo -- GLOBALS: DEFAULT_CHAT_FRAME, SELECTED_CHAT_FRAME local BANK_CONTAINER = BANK_CONTAINER -- local KEYRING_CONTAINER = KEYRING_CONTAINER local NUM_BANKBAGSLOTS = NUM_BANKBAGSLOTS local NUM_BAG_SLOTS = NUM_BAG_SLOTS local REAGENTBANK_CONTAINER = REAGENTBANK_CONTAINER -- no longer used: lib.frame = lib.frame or CreateFrame("frame", string.gsub(MAJOR,"[^%w]", "_").."_Frame") if lib.frame then lib.frame:Hide() lib.frame:UnregisterAllEvents() end ----------------------------------------------------------------------- -- General-purpose utilities: local t = {} local function print(...) local msg if select("#",...)>1 then for k=1,select("#",...) do t[k]=tostring(select(k,...)) end msg = tconcat(t, " ", 1, select("#",...)) else msg = ... end msg = gsub(msg, "\124", "\\124"); (SELECTED_CHAT_FRAME or DEFAULT_CHAT_FRAME):AddMessage(MAJOR..": "..msg) end local function escapePatterns(str) return ( gsub(str, "([-+.?*%%%[%]%(%)])", "%%%1") ) end ----------------------------------------------------------------------- -- makeLinkComparator() -- Take an itemnumber, name, itemstring, or full link, and return a (funcref,arg2,arg3) tuple that can be used to test against several itemlinks local floor=math.floor local function compareFuzzySuffix(link, pattern, uniq16) local uniq = strmatch(link, pattern) if not uniq then -- first 8 params didn't match return false end return floor(tonumber(uniq)/65536)==uniq16 end local function makeLinkComparator(lookingfor) if type(lookingfor)=="number" then -- "item:-12345" -> "item:%-12345[:|]" return strmatch, "|Hitem:"..escapePatterns(lookingfor).."[:|]",nil elseif type(lookingfor)=="string" then if strmatch(lookingfor, "^item:") or strmatch(lookingfor, "|H") then -- (convert to itemstring) and ensure there's no level info in it (9th param) local str = strmatch(lookingfor, "(item:.-:.-:.-:.-:.-:.-:.-:.-)[:|]") if not str then str = strmatch(lookingfor, "(item:[-0-9:]+)") else -- hokay, we have an itemstring. now we need to check for wobbly suffix factors thanks to 3.2 madness -- see http://www.wowwiki.com/ItemString#3.2_wotlk_randomstat_items_changing_their_suffix_factors local firsteight,uniq = strmatch(str, "(item:.-:.-:.-:.-:.-:.-:%-.-:)([-0-9]+)") if uniq then -- suffix was negative, so suffix factors can wobble (really only with wotlk items, not BC ones, but meh) return compareFuzzySuffix, "|H"..escapePatterns(firsteight).."([-0-9]+)[:|]", floor(tonumber(uniq)/65536) else -- unwobbly item, we're done, fall through end end if not str then error(MAJOR..": MakeLinkComparator(): '"..tostring(lookingfor).."' does not appear to be a valid itemstring / itemlink", 3) end return strmatch, "|H" .. escapePatterns(str) .. "[:|]",nil else -- put "|h[" and "]|h" around a name return strmatch, "|h%["..escapePatterns(lookingfor).."%]|h",nil end end error(MAJOR..": MakeLinkComparator(): Expected number or string", 3) end ----------------------------------------------------------------------- -- Internal slot locking utilities - unfortunately slots where we just dropped an item arent considered locked by the API until the server processes it and returns a bag update event, so we consider them locked for 2 seconds ourselves lib.slotLocks = {} local GetTime = GetTime local function lockSlot(bag,slot) local slots = lib.slotLocks[bag] or {} if not lib.slotLocks[bag] then lib.slotLocks[bag] = slots end slots[slot] = GetTime() end local function isLocked(bag,slot) local slots = lib.slotLocks[bag] if not slots then return false end return GetTime() - (slots[slot] or 0) < 2 end ----------------------------------------------------------------------- -- Own family/freeslots handling -- Pre 4.2: This was to handle the goddamn keyring that doesn't behave like anything else, -- but i'm keeping these functions in in case blizzard adds something new that behaves badly (tabard rack anyone?) local function GetContainerFamily(bag) --[[ pre 4.2 if bag==KEYRING_CONTAINER then return 256 end ]] local free,fam = GetContainerNumFreeSlots(bag) return fam end function lib:GetContainerFamily(bag) return GetContainerFamily(bag) end local function myGetContainerNumFreeSlots(bag) --[[ pre 4.2 if bag==KEYRING_CONTAINER then local free=0 for slot=1,GetContainerNumSlots(bag) do if not GetContainerItemLink(bag,slot) then free=free+1 end end return free,256 end ]] return GetContainerNumFreeSlots(bag) end function lib:GetContainerNumFreeSlots(bag) return myGetContainerNumFreeSlots(bag) end ----------------------------------------------------------------------- -- API :MakeLinkComparator("itemstring" or "itemLink" or "itemName" or itemId) -- -- Returns a comparator function and two arguments, that can be used to -- rapidly compare several itemlinks to a set search pattern. -- -- This comparator will -- 1) Ignore the 9th "level" parameter introduced in 3.0 -- 2) Correctly match items with changing stats in inventory vs AH/Mail/GBank -- see http://www.wowwiki.com/ItemString#3.2_wotlk_randomstat_items_changing_their_suffix_factors-- -- 3) Pick the smartest way to compare available -- -- local comparator,arg1,arg2 = LBU:MakeLinkComparator(myItemString) -- for _,itemLink in pairs(myItems) do -- if comparator(itemLink, arg1,arg2) then -- print(itemLink, "matches", myItemString) -- function lib:MakeLinkComparator(lookingfor) return makeLinkComparator(lookingfor) end ----------------------------------------------------------------------- -- API :IterateBags("which", itemFamily) -- -- which - string: "BAGS", "BANK", "BAGSBANK", "REAGENTBANK" -- itemFamily - number: bitmasked itemFamily; will accept combinations -- 0: will only iterate regular bags -- nil: will iterate all bags (including possible future special bags!) -- -- Returns an iterator that can be used in a for loop, e.g.: -- for bag in LBU:IterateBags("BAGS") do -- loop all carried bags (including backpack & possible future special bags) local bags = { BAGS = {}, BANK = {}, BAGSBANK = {}, REAGENTBANK = {}, } -- Carried bags for i=1,NUM_BAG_SLOTS do bags.BAGS[i]=i end bags.BAGS[BACKPACK_CONTAINER]=BACKPACK_CONTAINER -- bags.BAGS[KEYRING_CONTAINER]=KEYRING_CONTAINER -- Bank bags for i=NUM_BAG_SLOTS+1,NUM_BAG_SLOTS+NUM_BANKBAGSLOTS do bags.BANK[i]=i end bags.BANK[BANK_CONTAINER]=BANK_CONTAINER -- Both for k,v in pairs(bags.BAGS) do bags.BAGSBANK[k]=v end for k,v in pairs(bags.BANK) do bags.BAGSBANK[k]=v end -- Reagent Bank bags.REAGENTBANK[REAGENTBANK_CONTAINER] = REAGENTBANK_CONTAINER local function iterbags(tab, cur) cur = next(tab, cur) while cur do if GetContainerFamily(cur) then return cur end cur = next(tab, cur) end end local function iterbagsfam0(tab, cur) cur = next(tab, cur) while cur do local free,fam = GetContainerNumFreeSlots(cur) if fam==0 then return cur end cur = next(tab, cur) end end function lib:IterateBags(which, itemFamily) local baglist=bags[which] if not baglist then error([[Usage: LibBagUtils:IterateBags("which"[, itemFamily])]], 2) end if which == "REAGENTBANK" and not IsReagentBankUnlocked() then return function() end, baglist end if not itemFamily then return iterbags, baglist elseif itemFamily==0 then return iterbagsfam0, baglist else return function(tab, cur) cur = next(tab, cur) while cur do local fam = GetContainerFamily(cur) if fam and band(itemFamily,fam)~=0 then return cur end cur = next(tab, cur) end end, baglist end end ----------------------------------------------------------------------- -- API: CountSlots(which, itemFamily) -- -- which - string: "BAGS", "BANK", "BAGSBANK", "REAGENTBANK" -- itemFamily - bitmasked itemFamily; see :IterateBags -- -- Returns: numFreeSlots, numTotalSlots -- BANK is considered to have 0 slots if bank window is not open function lib:CountSlots(which, itemFamily) local baglist=bags[which] if not baglist then error([[Usage: LibBagUtils:IterateBags("which"[, itemFamily])]], 2) end local free,tot=0,0 if which == "REAGENTBANK" and not IsReagentBankUnlocked() then return free,tot end if not itemFamily then for bag in pairs(baglist) do free = free + myGetContainerNumFreeSlots(bag) tot = tot + GetContainerNumSlots(bag) end elseif itemFamily==0 then for bag in pairs(baglist) do local f,bagFamily = GetContainerNumFreeSlots(bag) if bagFamily==0 then free = free + f tot = tot + GetContainerNumSlots(bag) end end else for bag in pairs(baglist) do local f,bagFamily = myGetContainerNumFreeSlots(bag) if bagFamily and band(itemFamily,bagFamily)~=0 then free = free + f tot = tot + GetContainerNumSlots(bag) end end end return free,tot end ----------------------------------------------------------------------- -- API :IsBank(bag) -- -- bag - number: bag number -- -- Returns true if the given bag is a bank bag function lib:IsBank(bag, incReagentBank) return bag==BANK_CONTAINER or (bag>=NUM_BAG_SLOTS+1 and bag<=NUM_BAG_SLOTS+NUM_BANKBAGSLOTS) or (incReagentBank and lib:IsReagentBank(bag)) end ----------------------------------------------------------------------- -- API :IsReagentBank(bag) -- -- bag - number: bag number -- -- Returns true if the given bag is the reagent bank "bag" function lib:IsReagentBank(bag) return IsReagentBankUnlocked() and (bag==REAGENTBANK_CONTAINER and true or nil) or nil end ----------------------------------------------------------------------- -- API :Iterate("which"[, "lookingfor"]) -- -- which - string: "BAGS", "BANK", "BAGSBANK", "REAGENTBANK" -- lookingfor - OPTIONAL: itemLink, itemName, itemString or itemId(number) -- -- Returns an iterator that can be used in a for loop, e.g.: -- for bag,slot,link in LBU:Iterate("BAGS") do -- loop all slots in carried bags (including backpack & keyring) -- for bag,slot,link in LBU:Iterate("BAGSBANK", 29434) do -- find all badges of justice function lib:Iterate(which, lookingfor) if which == "REAGENTBANK" and not IsReagentBankUnlocked() then return function() end end local baglist=bags[which] if not baglist then error([[Usage: LibBagUtils:Iterate(which [, item])]], 2) end local bag,slot,curbagsize=nil,0,0 local function iterator() while slot>=curbagsize do bag = iterbags(baglist, bag) if not bag then return nil end curbagsize=GetContainerNumSlots(bag) or 0 slot=0 end slot=slot+1 return bag,slot,GetContainerItemLink(bag,slot) end if lookingfor==nil then return iterator else local comparator,arg1,arg2 = makeLinkComparator(lookingfor) return function() for bag,slot,link in iterator do if link and comparator(link, arg1,arg2) then return bag,slot,link end end end end end ----------------------------------------------------------------------- -- API :Find("where", "lookingfor", findLocked]) -- -- where - string: "BAGS", "BANK", "BAGSBANK", "REAGENTBANK" -- lookingfor - itemLink, itemName, itemString or itemId(number) -- findLocked - OPTIONAL: if true, will also return locked slots -- -- Returns: bag,slot,link or nil on failure function lib:Find(where,lookingfor,findLocked) if where == "REAGENTBANK" and not IsReagentBankUnlocked() then return nil end for bag,slot,link in lib:Iterate(where,lookingfor) do local _, itemCount, locked, _, _ = GetContainerItemInfo(bag,slot) if findLocked or not locked then return bag,slot,link end end end ----------------------------------------------------------------------- -- API :FindSmallestStack("where", "lookingfor"[, findLocked]) -- -- where - string: "BAGS", "BANK", "BAGSBANK", "REAGENTBANK" -- lookingfor - itemLink, itemName, itemString or itemId(number) -- findLocked - OPTIONAL: if true, will also return locked slots -- -- Returns: bag,slot,size or nil on failure function lib:FindSmallestStack(where,lookingfor,findLocked) if where == "REAGENTBANK" and not IsReagentBankUnlocked() then return nil end local smallest=9e9 local smbag,smslot for bag,slot in lib:Iterate(where,lookingfor) do local _, itemCount, locked, _, _ = GetContainerItemInfo(bag,slot) if itemCount and itemCount<smallest and (findLocked or not locked) then smbag=bag smslot=slot smallest=itemCount end end if smbag then return smbag,smslot,smallest end end ----------------------------------------------------------------------- -- API :PutItem("where"[, dontClearOnFail[, count]]) -- -- Put the item currently held by the cursor in the most suitable bag -- (considering specialty bags, already-existing stacks..) -- -- where - string: "BAGS", "BANK", "BAGSBANK", "REAGENTBANK" -- count - OPTIONAL: number: if given, PutItem() will attempt to stack the item on top of another suitable stack. This is not possible without knowing the count. -- dontClearOnFail - OPTIONAL: boolean: If the put operation fails due to no room, do NOT clear the cursor. (Note that some other wow client errors WILL clear the cursor) -- -- Returns: bag,slot or false for out-of-room -- 0,0 will be returned if the function is called without an item in the cursor -- nil when which is REAGENTBANK and the ReagentBank has not been unlocked local function putinbag(destbag) if where == "REAGENTBANK" and not IsReagentBankUnlocked() then return nil end for slot=1,GetContainerNumSlots(destbag) do if (not GetContainerItemInfo(destbag,slot)) and (not isLocked(destbag,slot)) then -- empty! PickupContainerItem(destbag,slot) if not CursorHasItem() then -- success! lockSlot(destbag,slot) return slot end -- If we get here, something is probably severely broken. But we keep looping hoping for the best. end end end function lib:PutItem(where, count, dontClearOnFail) if where == "REAGENTBANK" and not IsReagentBankUnlocked() then return nil end local cursorType,itemId,itemLink = GetCursorInfo() if cursorType~="item" then geterrorhandler()(MAJOR..": PutItem(): There was no item in the cursor.") return 0,0 -- we consider nothing-at-all successfully disposed of (0,0 contains nil) end local baglist=bags[where] if not baglist then error("Usage: LibBagUtils:PutItem(where[, count[, dontClearOnFail]])", 2) end -- FIRST: if we have a known count, and the item is stackable, we try putting it on top of something else (look for the BIGGEST stack to put it on top of for max packing!) if count and count>=1 then local _, _, _, _, _, _, _, itemStackCount = GetItemInfo(itemLink) if itemStackCount>1 and count<itemStackCount then local bestsize,bestbag,bestslot=0 for bag,slot in lib:Iterate(where, itemId) do -- Only look for itemId, not the full string; we assume everything of the same itemId is stackable. Looking at the full itemstring is futile since everything has unique IDs these days. local _, ciCount, ciLocked, _, _ = GetContainerItemInfo(bag,slot) if ciLocked then -- nope! elseif isLocked(bag,slot) then -- nope! elseif ciCount+count<=itemStackCount and ciCount>bestsize then bestsize=ciCount bestbag=bag bestslot=slot end end if bestbag then -- Place it! PickupContainerItem(bestbag,bestslot) if not CursorHasItem() then -- success! lockSlot(bestbag,bestslot) local _, ciCount, ciLocked, _, _ = GetContainerItemInfo(bestbag,bestslot) return bestbag,bestslot end -- if we got here, the item couldn't be placed on top of the other for some reason, possibly because our assumption about equal itemids being wrong -- either way, we fall down and continue looking for somewhere to put it end -- Fall down and look for empty slots instead end end -- Put the item in the first empty slot that it CAN be put in! local itemFam = GetItemFamily(itemLink) if itemFam~=0 and select(9,GetItemInfo(itemLink))=="INVTYPE_BAG" then itemFam = 0 -- Ouch, it was a bag. Bags are always family 0 for purposes of trying to PUT them somewhere. end -- If this is a specialty item, we try specialty bags first if itemFam~=0 then for bag in iterbags, baglist do local bagFree, bagFam = myGetContainerNumFreeSlots(bag) if bagFam~=0 and band(itemFam,bagFam)~=0 then local slot = putinbag(bag) if slot then return bag,slot end end end end -- If we couldn't put it in a special bag, try normal bags for bag in iterbagsfam0, baglist do if GetContainerNumFreeSlots(bag)>0 then local slot = putinbag(bag) if slot then return bag,slot end end end -- Set sail on the failboat! if not dontClearOnFail then ClearCursor() end return false -- no room for it! end ----------------------------------------------------------------------- -- API :LinkIsItem(fullLink, lookingfor) -- -- See if "lookingfor" equals the full link given. "lookingfor" can be any kind of item identifier. -- Level information is always ignored. Wobbly 3.2 randomstats are compensated for. function lib:LinkIsItem(fullLink, lookingfor) local comparator,arg1,arg2 = makeLinkComparator(lookingfor) return comparator(fullLink, arg1,arg2) end