Mercurial > wow > inventory
diff Modules/Mover.lua @ 110:67bd5057ecb7
Implemented vendor restocking with the mover. Comitting so I can always review this working version, but I?ll be disabling all part of it as it is not going to work properly without seriously compromising the code structure.
Debug messages are now appended with ?Inventorium? (my MailOpener addon was making stuff difficult).
Now properly removing the refill window from the displayed static popup windows list so new popups won?t be aligned at odd locations.
Changed ?CreateMoverFrame? to not contain any scenario-specific info. All settings can be set with SetFrameSettings.
Items that belong to speciality bags are now put there. Other items now ignore spaciality bags.
Implemented test code for mailbox refill support. It has been disabled due to some issues but may be introduced later.
The guild withdrawal limit is now taken into consideration.
Queue is now reset before scanning again.
author | Zerotorescue |
---|---|
date | Fri, 14 Jan 2011 23:25:05 +0100 |
parents | 3bbad0429d87 |
children | 41f0689dfda1 |
line wrap: on
line diff
--- a/Modules/Mover.lua Wed Jan 12 22:48:25 2011 +0100 +++ b/Modules/Mover.lua Fri Jan 14 23:25:05 2011 +0100 @@ -6,12 +6,82 @@ 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, numMissing, numAvailable) +addon.Locations = { + Bag = 0, + Bank = 1, + Guild = 2, + Mailbox = 3, + Merchant = 4, +}; + +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) + -- The below behavior was changed in patch 4.0.1, it now acts as expected; quantity requested equals exact quantity bought, even with increased batchsize + + -- Some merchant items are sold in batches (e.g. of 5) + -- In that case BuyMerchantItem wants the num stacks, rather than the quantity to be bought + --local batchSize = select(4, GetMerchantItemInfo(merchantIndex)); + + --local batches = math.ceil(num / batchSize); + + --BuyMerchantItem(merchantIndex, batches); + + 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, price) table.insert(queuedMoves, { - id = itemId, - num = amount, - missing = numMissing, - available = numAvailable, + ["itemId"] = itemId, + ["num"] = amount, -- can not be unlimited + ["missing"] = numMissing, + ["available"] = numAvailable, + ["price"] = price, }); end @@ -43,6 +113,46 @@ 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 + table.insert(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"); @@ -57,24 +167,52 @@ addon:Debug("%d moves were queued.", #queuedMoves); for _, singleMove in pairs(queuedMoves) do - local sourceItem = sourceContents[singleMove.id]; + 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.id)), addon.Colors.Red); + 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) table.sort(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 - local movingNum = ((itemLocation.count > singleMove.num and singleMove.num) or itemLocation.count); + 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 + + table.insert(outgoingMoves, { + ["itemId"] = singleMove.itemId, + ["num"] = stackSize, + ["container"] = itemLocation.container, + ["slot"] = itemLocation.slot, + }); + + movingNum = (movingNum - stackSize); + end + end + end table.insert(outgoingMoves, { - itemId = singleMove.id, - num = movingNum, - container = itemLocation.container, - slot = itemLocation.slot, + ["itemId"] = singleMove.itemId, + ["num"] = movingNum, + ["container"] = itemLocation.container, + ["slot"] = itemLocation.slot, }); singleMove.num = (singleMove.num - movingNum); @@ -92,56 +230,43 @@ -- 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 + local emptySlots = GetEmptySlots(); addon:Debug("%d empty slots are available.", #emptySlots); -- Remember where we're moving from movesSource = location; - local backup = 0; + 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 - -- A not equal-comparison should be quicker than a larger/smaller than-comparison + -- Repeat the below loop until nothing is remaining for _, outgoingMove in pairs(outgoingMoves) do - -- itemId will be set to nil when this outgoing move was processed - sanity check - if outgoingMove.itemId then + 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 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 + 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 firstAvailableSlot = emptySlots[1]; + 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 @@ -153,13 +278,15 @@ end end else + -- Consume empty slot + table.insert(combinedMoves, { - itemId = outgoingMove.itemId, - num = outgoingMove.num, - sourceContainer = outgoingMove.container, - sourceSlot = outgoingMove.slot, - targetContainer = firstAvailableSlot.container, - targetSlot = firstAvailableSlot.slot, + ["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, @@ -192,12 +319,12 @@ -- 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, + ["itemId"] = outgoingMove.itemId, + ["num"] = outgoingMove.num, + ["sourceContainer"] = outgoingMove.container, + ["sourceSlot"] = outgoingMove.slot, + ["targetContainer"] = itemLocation.container, + ["targetSlot"] = itemLocation.slot, }); itemLocation.count = (itemLocation.count + outgoingMove.num); @@ -208,12 +335,12 @@ -- 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, + ["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 @@ -251,7 +378,6 @@ 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(); @@ -273,41 +399,7 @@ onFinish(); end -local ContainerFunctions = { - [addon.Locations.Bag] = { - GetItemId = GetContainerItemID, - PickupItem = SplitContainerItem, - IsLocked = function(sourceContainer, sourceSlot) - return select(3, GetContainerItemInfo(sourceContainer, sourceSlot); - end, - }, - [addon.Locations.Bank] = { - GetItemId = GetContainerItemID, - PickupItem = SplitContainerItem, - IsLocked = function(sourceContainer, sourceSlot) - return select(3, GetContainerItemInfo(sourceContainer, sourceSlot); - end, - }, - [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, - }, - --[[ Even though support is possible, it will require a little more work than just this and there are currently higher priorities - [addon.Locations.Mailbox] = { - GetItemId = function(mailIndex, attachmentId) - return addon:GetItemId(GetInboxItemLink(mailIndex, attachmentId)); - end, - PickupItem = TakeInboxItem, - IsLocked = function() return false; end, - DoNotDrop = true, - },]] -}; - +local tmrRetry; function mod:ProcessMove() addon:Debug("ProcessMove"); @@ -317,12 +409,23 @@ 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(); - self:RegisterEvent("ITEM_LOCK_CHANGED"); + if movesSource == addon.Locations.Mailbox then + MailAddonBusy = addon:GetName(); + end + + self:RegisterEvent(ContainerFunctions[movesSource].Event, "SourceUpdated"); self:RegisterEvent("UI_ERROR_MESSAGE"); -- combinedMoves now has all moves in it (source -> target) @@ -333,20 +436,24 @@ local sourceLocationsLocked = {}; local targetLocationsLocked = {}; + local hasMoved; + 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 ContainerFunctions[movesSource].IsLocked(move.sourceContainer, move.sourceSlot)); + -- 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 then + 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; @@ -355,34 +462,47 @@ -- 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 movesSource ~= addon.Locations.Bank or CursorHasItem() then -- CursorHasItem only works when moving outside of the bank - -- We are moving into our local bags, so the below must check normal - local targetItemId = ContainerFunctions[movesSource].get(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"); + 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 + table.remove(combinedMoves, numCurrentMove); + else + self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false"); 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; + else + -- When items are deposit automatically we still need to remember when a move has been processed -- 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 @@ -402,7 +522,7 @@ end local tmrProcessNext; -function mod:ITEM_LOCK_CHANGED() +function mod:SourceUpdated() self:CancelTimer(tmrProcessNext, true); -- silent tmrProcessNext = self:ScheduleTimer("ProcessMove", .5); end @@ -416,27 +536,35 @@ 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 - table.wipe(combinedMoves); - movesSource = nil; -- Make sure nothing is at the mouse ClearCursor(); -- Stop timer - self:UnregisterEvent("ITEM_LOCK_CHANGED"); + self:UnregisterEvent(ContainerFunctions[movesSource].Event); self:CancelTimer(tmrProcessNext, true); -- silent self:UnregisterEvent("UI_ERROR_MESSAGE"); + + -- Reset vars + table.wipe(combinedMoves); + movesSource = nil; + if MailAddonBusy == addon:GetName() then + MailAddonBusy = nil; + end end function mod:OnEnable()