Mercurial > wow > inventory
view Modules/Mover.lua @ 131:a27948591159
Fixed premade group updating.
Autorefill options now indicate storage rather than (guild) bank (as this now includes the mailbox too).
author | Zerotorescue |
---|---|
date | Tue, 18 Jan 2011 00:28:24 +0100 |
parents | 6724bc8eface |
children | 84e38318f569 |
line wrap: on
line source
local addon = select(2, ...); local mod = addon:NewModule("Mover", "AceEvent-3.0", "AceTimer-3.0"); local _G = _G; local select, pairs = _G.select, _G.pairs; local tinsert, twipe, treverse, tsort, tremove = _G.table.insert, _G.table.wipe, _G.table.reverse, _G.table.sort, _G.table.remove; local Scanner; local queuedMoves = {}; -- table storing all queued moves before BeginMove is called local combinedMoves = {}; -- table storing all combined moves (with source and target) that is to be processed by the actual mover in the order of the index (1 to #) local movesSource; local ContainerFunctions = { [addon.Locations.Bag] = { GetItemId = GetContainerItemID, PickupItem = SplitContainerItem, IsLocked = function(sourceContainer, sourceSlot) return select(3, GetContainerItemInfo(sourceContainer, sourceSlot)); end, Event = "ITEM_LOCK_CHANGED", }, [addon.Locations.Bank] = { GetItemId = GetContainerItemID, PickupItem = SplitContainerItem, IsLocked = function(sourceContainer, sourceSlot) return select(3, GetContainerItemInfo(sourceContainer, sourceSlot)); end, Event = "ITEM_LOCK_CHANGED", }, [addon.Locations.Guild] = { GetItemId = function(tabId, slotId) return addon:GetItemId(GetGuildBankItemLink(tabId, slotId)); end, PickupItem = SplitGuildBankItem, IsLocked = function(sourceContainer, sourceSlot) return select(3, GetGuildBankItemInfo(sourceContainer, sourceSlot)); end, Event = "ITEM_LOCK_CHANGED", }, [addon.Locations.Mailbox] = { GetItemId = function(mailIndex, attachmentId) return addon:GetItemId(GetInboxItemLink(mailIndex, attachmentId)); end, PickupItem = TakeInboxItem, IsLocked = function() return false; end, DoNotDrop = true, -- TakeInboxItem does not support picking up Synchronous = true, -- wait after every single move Event = "BAG_UPDATE", }, [addon.Locations.Merchant] = { GetItemId = function(_, merchantIndex) return addon:GetItemId(GetMerchantItemLink(merchantIndex)); end, PickupItem = function(_, merchantIndex, num) return BuyMerchantItem(merchantIndex, num); end, IsLocked = function() return false; end, DoNotDrop = true, -- BuyMerchantItem does not support picking up Burst = true, -- spam buy items, the source can take it Event = "BAG_UPDATE", }, }; function mod:AddMove(itemId, amount, numMissing, numAvailable, cost) tinsert(queuedMoves, { ["itemId"] = itemId, ["num"] = amount, -- can not be unlimited ["missing"] = numMissing, ["available"] = numAvailable, ["cost"] = cost, }); end function mod:HasMoves() return (#queuedMoves ~= 0); end function mod:GetMoves() return queuedMoves; end function mod:ResetQueue() twipe(queuedMoves); end if not treverse then treverse = function(orig) local temp = {}; local origLength = #orig; for i = 1, origLength do temp[(origLength - i + 1)] = orig[i]; end -- -- Update the original table (can't do orig = temp as that would change the reference-link instead of the original table) -- for i, v in pairs(temp) do -- orig[i] = v; -- end return temp; -- for speed we choose to do a return instead end end local function GetEmptySlots() local emptySlots = {}; -- Go through all our bags, including the backpack for bagId = 0, NUM_BAG_SLOTS do -- Go through all our slots (0 = backpack) for slotId = 1, GetContainerNumSlots(bagId) do local itemId = GetContainerItemID(bagId, slotId); -- we're scanning our local bags here, so no need to get messy with guild bank support local bagFamily = select(2, GetContainerNumFreeSlots(bagId)); if not itemId then tinsert(emptySlots, { ["container"] = bagId, ["slot"] = slotId, ["family"] = bagFamily, }); end end end return emptySlots; end local function GetFirstEmptySlot(emptySlots, prefFamily) while prefFamily do for _, slot in pairs(emptySlots) do if slot.family == prefFamily then return slot; end end -- If this was a special family, no special bag available, now check normal bags if prefFamily > 0 then prefFamily = 0; else prefFamily = nil; end end end function mod:BeginMove(location, onFinish) addon:Debug("BeginMove"); -- Find the outgoing moves -- We need the source container and slot, find all the requires sources and put them in a list which we go through later to find matching targets -- Get a list of items in the source container local sourceContents = Scanner:CacheLocation(location, false); local outgoingMoves = {}; addon:Debug("%d moves were queued.", #queuedMoves); for _, singleMove in pairs(queuedMoves) do local sourceItem = sourceContents[singleMove.itemId]; if not sourceItem then addon:Print(("Can't move %s, this doesn't exist in the source."):format(IdToItemLink(singleMove.itemId)), addon.Colors.Red); else -- We want to move the smallest stacks first to keep stuff pretty (and minimize space usage, splitting a stack takes 2 slots, moving something only 1) tsort(sourceItem.locations, function(a, b) -- -1 indicates unlimited, this is always more than an actual amount if a.count == -1 then return false; elseif b.count == -1 then return true; end return a.count < b.count; end); for _, itemLocation in pairs(sourceItem.locations) do -- if this location has more items than we need, only move what we need, otherwise move everything in this stack -- -1 items indicates unlimited amount, in that case we must cap at missing items local movingNum = (((itemLocation.count == -1 or itemLocation.count > singleMove.num) and singleMove.num) or itemLocation.count); if itemLocation.count == -1 then -- If the source has an unlimited quantity, makes moves based on the max stacksize local stackSize = select(8, GetItemInfo(singleMove.itemId)); -- 8 = stacksize if stackSize then while movingNum > stackSize do -- Move a single stack size while the amount remaining to be moved is above the stack size num tinsert(outgoingMoves, { ["itemId"] = singleMove.itemId, ["num"] = stackSize, ["container"] = itemLocation.container, ["slot"] = itemLocation.slot, }); movingNum = (movingNum - stackSize); singleMove.num = (singleMove.num - stackSize); end end end tinsert(outgoingMoves, { ["itemId"] = singleMove.itemId, ["num"] = movingNum, ["container"] = itemLocation.container, ["slot"] = itemLocation.slot, }); singleMove.num = (singleMove.num - movingNum); if singleMove.num == 0 then -- If we have prepared everything we wanted, go to the next queued move break; -- stop the locations-loop end end end end addon:Debug("%d outgoing moves are possible.", #outgoingMoves); -- No longer needed twipe(queuedMoves); -- Process every single outgoing move and find fitting targets -- Get a list of items already in the target container local targetContents = Scanner:CacheLocation(addon.Locations.Bag, false); -- Find all empty slots local emptySlots = GetEmptySlots(); addon:Debug("%d empty slots are available.", #emptySlots); -- Remember where we're moving from movesSource = location; local backup = 0; -- the below loop should never break, but if it does for any reason, this is here to stop it from freezing the game while #outgoingMoves ~= 0 do -- Repeat the below loop until nothing is remaining for _, outgoingMove in pairs(outgoingMoves) do if outgoingMove.itemId then -- itemId will be set to nil when this outgoing move was processed - sanity check local targetItem = targetContents[outgoingMove.itemId]; if not targetItem or ContainerFunctions[location].DoNotDrop then -- There is no partial stack which can be filled or this source container doesn't allow manual allocation of items (in which case we always assume it takes a full empty slot) local family = GetItemFamily(outgoingMove.itemId); if family and family ~= 0 and select(9, GetItemInfo(outgoingMove.itemId)) == "INVTYPE_BAG" then -- Containers can only fit in general slots but GetItemFamily will return what they can contain themselves family = 0; end local firstAvailableSlot = GetFirstEmptySlot(emptySlots, family); if not firstAvailableSlot then -- No empty slot available - bags are full addon:Print(("Bags are full. Skipping %s."):format(IdToItemLink(outgoingMove.itemId)), addon.Colors.Orange); outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table -- Not a single item with this item id can be moved, since we only want the bags are full announcement once, we remove all other moves with this item id for _, otherMove in pairs(outgoingMoves) do if otherMove.itemId and otherMove.itemId == outgoingMove.itemId then otherMove.itemId = nil; end end else -- Consume empty slot tinsert(combinedMoves, { ["itemId"] = outgoingMove.itemId, ["num"] = outgoingMove.num, ["sourceContainer"] = outgoingMove.container, ["sourceSlot"] = outgoingMove.slot, ["targetContainer"] = firstAvailableSlot.container, ["targetSlot"] = firstAvailableSlot.slot, }); -- We filled an empty slot so the target contents now has one more item, -- make a new instance of the ItemMove class so any additional items with this id can be stacked on top of it local itemMove = addon.ContainerItem:New(); itemMove:AddLocation(firstAvailableSlot.container, firstAvailableSlot.slot, outgoingMove.num); targetContents[outgoingMove.itemId] = itemMove; tremove(emptySlots, 1); -- no longer empty outgoingMove.num = 0; -- nothing remaining - sanity check outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table end else -- Find the maximum stack size for this item local itemStackCount = select(8, GetItemInfo(outgoingMove.itemId)); -- We want to move to the largest stacks first to keep stuff pretty tsort(targetItem.locations, function(a, b) return a.count > b.count; end); for _, itemLocation in pairs(targetItem.locations) do if itemLocation.count < itemStackCount and outgoingMove.num > 0 then -- Check if this stack isn't already full (and we still need to move this item) local remainingSpace = (itemStackCount - itemLocation.count); if remainingSpace >= outgoingMove.num then -- Enough room to move this entire stack -- Deposit this item and then forget this outgoing move as everything in it was processed tinsert(combinedMoves, { ["itemId"] = outgoingMove.itemId, ["num"] = outgoingMove.num, ["sourceContainer"] = outgoingMove.container, ["sourceSlot"] = outgoingMove.slot, ["targetContainer"] = itemLocation.container, ["targetSlot"] = itemLocation.slot, }); itemLocation.count = (itemLocation.count + outgoingMove.num); outgoingMove.num = 0; -- nothing remaining outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table break; -- stop the locations-loop else -- Deposit this item but don't remove the outgoing move as there are some items left to move tinsert(combinedMoves, { ["itemId"] = outgoingMove.itemId, ["num"] = outgoingMove.num, ["sourceContainer"] = outgoingMove.container, ["sourceSlot"] = outgoingMove.slot, ["targetContainer"] = itemLocation.container, ["targetSlot"] = itemLocation.slot, }); -- The target will be full when we complete, but the source will still have remaining items left to be moved itemLocation.count = itemStackCount; outgoingMove.num = (outgoingMove.num - remainingSpace); end end end if outgoingMove.num > 0 then -- We went through all matching items and checked their stack sizes if we could move this there, no room available -- So forget about the target item (even though it may just have full locations, these are useless anyway) and the next loop move it onto an empty slot targetContents[outgoingMove.itemId] = nil; end end end end -- Loop through the array to find items that should be removed, start with the last element or the loop would break local numOutgoingMoves = #outgoingMoves; -- since LUA-tables start at an index of 1, this is actually an existing index (outgoingMoves[#outgoingMoves] would return a value) while numOutgoingMoves ~= 0 do -- A not equal-comparison should be quicker than a larger/smaller than-comparison -- Check if the item id is nil, this is set to nil when this outgoing move has been processed if not outgoingMoves[numOutgoingMoves].itemId or outgoingMoves[numOutgoingMoves].num == 0 then -- Remove this element from the array tremove(outgoingMoves, numOutgoingMoves); end -- Proceed with the next element (or previous considering we're going from last to first) numOutgoingMoves = (numOutgoingMoves - 1); end addon:Debug("%d moves remaining.", #outgoingMoves); backup = (backup + 1); if backup > 1000 then twipe(outgoingMoves); self:Abort("mover crashed", "Error preparing moves, hit an endless loop"); onFinish(); return; end end -- Reverse table, we need to go through it from last to first because we'll be removing elements, but we don't want the actions to be executed in a different order combinedMoves = treverse(combinedMoves); addon:Debug("%d moves should be possible.", #combinedMoves); -- No longer needed twipe(emptySlots); self:ProcessMove(); -- Even though we aren't completely done yet, allow requeueing onFinish(); end local tmrRetry; function mod:ProcessMove() addon:Debug("ProcessMove"); if #combinedMoves == 0 then addon:Print("Nothing to move."); self:Abort(); return; elseif movesSource == addon.Locations.Mailbox and MailAddonBusy and MailAddonBusy ~= addon:GetName() then addon:Debug("Anoter addon (%s) is busy with the mailbox.", MailAddonBusy); self:CancelTimer(tmrRetry, true); -- silent tmrRetry = self:ScheduleTimer("ProcessMove", .5); return; end -- Make sure nothing is at the mouse ClearCursor(); if movesSource == addon.Locations.Mailbox then MailAddonBusy = addon:GetName(); -- Since mailbox indexes change as mail is emptied (emptied mail is automatically deleted, thus number 50 would become 49 when 1 disappears), we must start with the last mail first tsort(combinedMoves, function(a, b) return a.sourceContainer < b.sourceContainer; end); end self:RegisterEvent(ContainerFunctions[movesSource].Event, "SourceUpdated"); self:RegisterEvent("UI_ERROR_MESSAGE"); -- combinedMoves now has all moves in it (source -> target) -- go through list, move everything inside it -- add source and target to lists, if either is already in this list, skip the move -- repeat every few seconds until we're completely done local sourceLocationsLocked = {}; local targetLocationsLocked = {}; local hasMoved; local combinedMovesOriginalLength = #combinedMoves; local numCurrentMove = combinedMovesOriginalLength; while numCurrentMove ~= 0 do local move = combinedMoves[numCurrentMove]; -- Only check if the source is locked when we're not bursting (some functions allow mass calling simultaneously and don't trigger item locks) local isSourceLocked = ((not ContainerFunctions[movesSource].Burst and sourceLocationsLocked[move.sourceContainer] and sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) or ContainerFunctions[movesSource].IsLocked(move.sourceContainer, move.sourceSlot)); -- Target are always the local bags local isTargetLocked = ((targetLocationsLocked[move.targetContainer] and targetLocationsLocked[move.targetContainer][move.targetSlot]) or ContainerFunctions[addon.Locations.Bag].IsLocked(move.targetContainer, move.targetSlot)); if move and not isSourceLocked and not isTargetLocked and (not ContainerFunctions[movesSource].Synchronous or not hasMoved) then addon:Print(("Moving %dx%s."):format(move.num, IdToItemLink(move.itemId))); addon:Debug("Moving %dx%s from (%d,%d) to (%d,%d)", move.num, IdToItemLink(move.itemId), move.sourceContainer, move.sourceSlot, move.targetContainer, move.targetSlot); -- Check if the source has been changed since out scan if ContainerFunctions[movesSource].GetItemId(move.sourceContainer, move.sourceSlot) ~= move.itemId then self:Abort("source changed", "Source (" .. move.sourceContainer .. "," .. move.sourceSlot .. ") is not " .. IdToItemLink(move.itemId)); return; end -- Pickup stack ContainerFunctions[movesSource].PickupItem(move.sourceContainer, move.sourceSlot, move.num); hasMoved = true; -- Remember we picked this item up and thus it is now locked if not sourceLocationsLocked[move.sourceContainer] then sourceLocationsLocked[move.sourceContainer] = {}; end sourceLocationsLocked[move.sourceContainer][move.sourceSlot] = true; if not ContainerFunctions[movesSource].DoNotDrop then -- Some sources don't actually pick items up but just move them, this makes the code below unnessary if movesSource ~= addon.Locations.Bank or CursorHasItem() then -- CursorHasItem only works when moving from the bank -- We are moving into our local bags, so the below must check normal bags -- Check if the target has been changed since out scan local targetItemId = ContainerFunctions[addon.Locations.Bag].GetItemId(move.targetContainer, move.targetSlot); if targetItemId and targetItemId ~= move.itemId then self:Abort("target changed", "Target (" .. move.targetContainer .. "," .. move.targetSlot .. ") is not " .. IdToItemLink(move.itemId) .. " nor empty"); return; end -- And drop it (this is always a local bag so no need to do any guild-checks) PickupContainerItem(move.targetContainer, move.targetSlot); -- Remember we dropped an item here and thus this is now locked if not targetLocationsLocked[move.targetContainer] then targetLocationsLocked[move.targetContainer] = {}; end targetLocationsLocked[move.targetContainer][move.targetSlot] = true; -- This move was processed tremove(combinedMoves, numCurrentMove); else self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false"); return; end else -- When items are deposit automatically we still need to remember when a move has been processed -- This move was processed tremove(combinedMoves, numCurrentMove); end end -- Proceed with the next element (or previous considering we're going from last to first) numCurrentMove = (numCurrentMove - 1); end addon:Debug("%d moves processed. %d moves remaining.", (combinedMovesOriginalLength - #combinedMoves), #combinedMoves); if #combinedMoves == 0 then addon:Print("Finished.", addon.Colors.Green); self:Abort(); return; end end local tmrProcessNext; function mod:SourceUpdated() self:CancelTimer(tmrProcessNext, true); -- silent tmrProcessNext = self:ScheduleTimer("ProcessMove", .5); end function IdToItemLink(itemId) local itemLink = select(2, GetItemInfo(itemId)); itemLink = itemLink or "Unknown (" .. itemId .. ")"; return itemLink; end function mod:UI_ERROR_MESSAGE(e, errorMessage) if errorMessage == ERR_SPLIT_FAILED then self:Abort("splitting failed", "Splitting failed."); elseif errorMessage == ERR_GUILD_WITHDRAW_LIMIT then self:Abort("at guild withdrawal limit", "At guild withdrawal limit"); end end function mod:Abort(simple, debugMsg) -- Announce if debugMsg then addon:Debug("Aborting:%s", debugMsg); end if simple then addon:Print(("Aborting: %s."):format(simple), addon.Colors.Red); end -- Make sure nothing is at the mouse ClearCursor(); -- Stop timer self:UnregisterEvent(ContainerFunctions[movesSource].Event); self:CancelTimer(tmrProcessNext, true); -- silent self:UnregisterEvent("UI_ERROR_MESSAGE"); -- Reset vars twipe(combinedMoves); movesSource = nil; if MailAddonBusy == addon:GetName() then MailAddonBusy = nil; end end function mod:OnEnable() Scanner = addon:GetModule("Scanner"); end function mod:OnDisable() Scanner = nil; self:Abort(); end