Mercurial > wow > inventory
view Modules/Mover.lua @ 92:88898c1e9e61
Fixed queueing of items which have none at the AH.
author | Zerotorescue |
---|---|
date | Fri, 07 Jan 2011 23:14:33 +0100 |
parents | a12d22ef3f39 |
children | 31493364b163 |
line wrap: on
line source
local addon = select(2, ...); local mod = addon:NewModule("Mover", "AceEvent-3.0", "AceTimer-3.0"); 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; function mod:AddMove(itemId, amount) table.insert(queuedMoves, { id = itemId, num = amount, }); end function mod:HasMoves() return (#queuedMoves ~= 0); end if not table.reverse then table.reverse = 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 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.id]; if not sourceItem then print("Can't move " .. IdToItemLink(singleMove.id) .. ", non-existant in source"); 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) table.sort(sourceItem.locations, function(a, b) 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 local movingNum = ((itemLocation.count > singleMove.num and singleMove.num) or itemLocation.count); table.insert(outgoingMoves, { itemId = singleMove.id, 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 table.wipe(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 = {}; local start = 0; local stop = NUM_BAG_SLOTS; -- Go through all our bags, including the backpack for bagId = start, stop do -- Go through all our slots 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 if not itemId then table.insert(emptySlots, { container = bagId, slot = slotId, }); end end end addon:Debug("%d empty slots are available.", #emptySlots); -- Remember where we're moving from movesSource = location; local backup = 0; while #outgoingMoves ~= 0 do -- A not equal-comparison should be quicker than a larger/smaller than-comparison for _, outgoingMove in pairs(outgoingMoves) do -- itemId will be set to nil when this outgoing move was processed - sanity check if outgoingMove.itemId then local targetItem = targetContents[outgoingMove.itemId]; if not targetItem then -- grab an empty slot -- make new instance of ItemMove -- populate targetContents with it so future moves of this item can be put on top of it if this isn't a full stack local firstAvailableSlot = emptySlots[1]; if not firstAvailableSlot then print("Bags are full. Skipping " .. IdToItemLink(outgoingMove.itemId) .. "."); outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table else table.insert(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; table.remove(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 table.sort(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 table.insert(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 table.insert(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 table.remove(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 dump(nil, outgoingMoves); table.wipe(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 = table.reverse(combinedMoves); addon:Debug("%d moves should be possible.", #combinedMoves); -- No longer needed table.wipe(emptySlots); self:ProcessMove(); -- Even though we aren't completely done yet, allow requeueing onFinish(); end function mod:ProcessMove() addon:Debug("ProcessMove"); if #combinedMoves == 0 then print("Nothing to move."); self:Abort(); return; end --self:RegisterEvent("BAG_UPDATE"); self:RegisterEvent("ITEM_LOCK_CHANGED"); 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 _GetContainerItemId = GetContainerItemID; if movesSource == addon.Locations.Guild then _GetContainerItemId = function(tabId, slotId) return addon:GetItemID(GetGuildBankItemLink(tabId, slotId)); end; end local _GetContainerItemInfo = GetContainerItemInfo; if movesSource == addon.Locations.Guild then _GetContainerItemInfo = GetGuildBankItemInfo; end local combinedMovesOriginalLength = #combinedMoves; local numCurrentMove = combinedMovesOriginalLength; while numCurrentMove ~= 0 do local move = combinedMoves[numCurrentMove]; local isSourceLocked = ((sourceLocationsLocked[move.sourceContainer] and sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) or select(3, _GetContainerItemInfo(move.sourceContainer, move.sourceSlot))); local isTargetLocked = ((targetLocationsLocked[move.targetContainer] and targetLocationsLocked[move.targetContainer][move.targetSlot]) or select(3, GetContainerItemInfo(move.targetContainer, move.targetSlot))); if move and not isSourceLocked and not isTargetLocked then 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); if _GetContainerItemId(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 if movesSource == addon.Locations.Bank then SplitContainerItem(move.sourceContainer, move.sourceSlot, move.num); elseif movesSource == addon.Locations.Guild then SplitGuildBankItem(move.sourceContainer, move.sourceSlot, move.num); end -- 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 movesSource == addon.Locations.Guild or CursorHasItem() then -- CursorHasItem is always false if source is a guild tab -- We are moving into our local bags, so the below must check normal local targetItemId = GetContainerItemID(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 table.remove(combinedMoves, numCurrentMove); else self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false"); return; 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 print("Finished."); self:Abort(); return; end end local tmrProcessNext; function mod:ITEM_LOCK_CHANGED() 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."); end end function mod:Abort(simple, debugMsg) if debugMsg then addon:Debug("Aborting:%s", debugMsg); end if simple then print("|cffff0000Aborting: " .. simple .. ".|r"); end table.wipe(combinedMoves); movesSource = nil; -- Stop timer self:UnregisterEvent("ITEM_LOCK_CHANGED"); self:CancelTimer(tmrProcessNext, true); -- silent self:UnregisterEvent("UI_ERROR_MESSAGE"); end function mod:OnEnable() Scanner = addon:GetModule("Scanner"); end function mod:OnDisable() Scanner = nil; self:Abort(); end