# HG changeset patch # User Zerotorescue # Date 1295043905 -3600 # Node ID 67bd5057ecb7190ac0d39756370e29d7d60a176f # Parent 3bbad0429d8704af9ef1092331f40859a6cdeb0e 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. diff -r 3bbad0429d87 -r 67bd5057ecb7 Classes/ContainerItem.class.lua --- a/Classes/ContainerItem.class.lua Wed Jan 12 22:48:25 2011 +0100 +++ b/Classes/ContainerItem.class.lua Fri Jan 14 23:25:05 2011 +0100 @@ -19,14 +19,30 @@ return self; end -function addon.ContainerItem:AddLocation(container, slot, count) +function addon.ContainerItem:AddLocation(container, slot, count, price) table.insert(self.locations, { - container = container, - slot = slot, - count = count, + ["container"] = container, + ["slot"] = slot, + ["count"] = count, + ["price"] = price, }); - self.totalCount = (self.totalCount + count); + -- -1 indicates unlimited supply + if self.totalCount ~= -1 then + if count == -1 then + self.totalCount = -1; + else + self.totalCount = (self.totalCount + count); + end + end return true; end + +function addon.ContainerItem:GetVendorPrice() + for _, loc in pairs(self.locations) do + if loc.price then + return loc.price; + end + end +end diff -r 3bbad0429d87 -r 67bd5057ecb7 Core.lua --- a/Core.lua Wed Jan 12 22:48:25 2011 +0100 +++ b/Core.lua Fri Jan 14 23:25:05 2011 +0100 @@ -494,6 +494,6 @@ end if self.debugChannel then - self.debugChannel:AddMessage(sformat(t, ...)); + self.debugChannel:AddMessage("|cffffff00Inventorium|r:" .. sformat(t, ...)); end end diff -r 3bbad0429d87 -r 67bd5057ecb7 Frames.lua --- a/Frames.lua Wed Jan 12 22:48:25 2011 +0100 +++ b/Frames.lua Fri Jan 14 23:25:05 2011 +0100 @@ -25,7 +25,7 @@ GameTooltip:Hide(); end -function addon:CreateMoverFrame(onAccept, onCancel) +function addon:CreateMoverFrame() local frameWidth = 400; -- Main window @@ -78,6 +78,9 @@ PlaySound("OrcExploration"); end); + frame:SetScript("OnHide", function(this) + StaticPopup_CollapseTable(this); + end); -- Title (AceGUI frame-widget-title used as example) local titleBackground = frame:CreateTexture(nil, "OVERLAY"); @@ -109,7 +112,8 @@ local lblTitle = frmTitle:CreateFontString(nil, "OVERLAY", "GameFontNormal"); lblTitle:SetPoint("TOP", titleBackground, "TOP", 0, -14); - lblTitle:SetText("Inventorium Bank Refill"); + + frame.lblTitle = lblTitle; -- Resizer (vertical only) local frmResizer = CreateFrame("Frame", nil, frame); @@ -126,7 +130,6 @@ lblDescription:SetWidth(frameWidth - 15 - 15); -- 10 margin left & 10 margin right lblDescription:SetJustifyH("LEFT"); lblDescription:SetJustifyV("TOP"); - lblDescription:SetText("The items listed below can be refilled from this location, do you wish to move them to your bags?"); frame.lblDescription = lblDescription; @@ -137,11 +140,9 @@ btnMove:SetWidth(125); btnMove:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 15, 11); btnMove:SetText("Move Items"); - btnMove:SetScript("OnClick", onAccept); + btnMove:SetScript("OnClick", function(this) this.OnClick(this); end); btnMove:SetScript("OnEnter", ShowTooltip); btnMove:SetScript("OnLeave", HideTooltip); - btnMove.tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Move Items"); - btnMove.tooltip = (not addon.db.profile.defaults.hideHelp and "Start moving these items from the bank."); frame.btnMove = btnMove; @@ -150,12 +151,9 @@ btnCancel:SetHeight(21); btnCancel:SetWidth(125); btnCancel:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 11); - btnCancel:SetText("Cancel"); - btnCancel:SetScript("OnClick", onCancel); + btnCancel:SetScript("OnClick", function(this) this.OnClick(this); end); btnCancel:SetScript("OnEnter", ShowTooltip); btnCancel:SetScript("OnLeave", HideTooltip); - btnCancel.tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Cancel"); - btnCancel.tooltip = (not addon.db.profile.defaults.hideHelp and "Do not move anything and close the window."); frame.btnCancel = btnCancel; @@ -168,61 +166,11 @@ frame.frmMeasureDummy = frmMeasureDummy; -- Scrolling table with a list of items to be moved - local scrollTableWidth = ( frame.frmMeasureDummy:GetWidth() - 30 ); -- adjust width by the scrollbar size - local headers = { - { - ["name"] = "Item", - ["width"] = (scrollTableWidth * .60), - ["defaultsort"] = "asc", - ["comparesort"] = function(this, aRow, bRow, column) - local aName, _, aRarity = GetItemInfo(this:GetRow(aRow).rowData.id); - local bName, _, bRarity = GetItemInfo(this:GetRow(bRow).rowData.id); - local template = "%d%s"; - aName = template:format((10 - (aRarity or 10)), (aName or ""):lower()); - bName = template:format((10 - (bRarity or 10)), (bName or ""):lower()); - - if this.cols[column].sort == "dsc" then - return aName > bName; - else - return aName < bName; - end - end, - ["sort"] = "asc", -- when the data is set, use this column so sort the default data - ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"), - ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by item quality then item name."), - }, - { - ["name"] = "Moving", - ["width"] = (scrollTableWidth * .15), - ["align"] = "RIGHT", - ["defaultsort"] = "dsc", - ["sortnext"] = 1, - ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Moving"), - ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the amount of movable items."), - }, - { - ["name"] = "Available", - ["width"] = (scrollTableWidth * .25), - ["align"] = "RIGHT", - ["defaultsort"] = "dsc", - ["sortnext"] = 1, - ["comparesort"] = function(this, aRow, bRow, column) - local aAvailablePercent = (this:GetRow(aRow).rowData.available / this:GetRow(aRow).rowData.missing); - local bAvailablePercent = (this:GetRow(bRow).rowData.available / this:GetRow(bRow).rowData.missing); - - if this.cols[column].sort == "dsc" then - return aAvailablePercent > bAvailablePercent; - else - return aAvailablePercent < bAvailablePercent; - end - end, - ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"), - ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the availibility percentage."), - }, - }; - local ScrollingTable = LibStub("ScrollingTable"); - local table = ScrollingTable:CreateST(headers, 3, 15, nil, frame); + local table = ScrollingTable:CreateST({}, 4, 15, nil, frame); -- inserting a dummy cols, real cols to be set in SetFrameSettings + table.frame:SetPoint("TOP", frame.lblDescription, "BOTTOM", 0, -18); + table.frame:SetPoint("LEFT", frame, "LEFT", 15, 0); + table.frame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 35); -- When moving over a row, provide a tooltip for the item table:RegisterEvents({ ["OnEnter"] = function(rowFrame, cellFrame, data, cols, row, realrow, column, scrollingTable, ...) @@ -256,14 +204,11 @@ }); frame.scrollTable = table; - table.frame:SetPoint("TOP", frame.lblDescription, "BOTTOM", 0, -18); - table.frame:SetPoint("LEFT", frame, "LEFT", 15, 0); - table.frame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 35); - + -- Change the amount of displayed rows based on the size of the frame frame.AdjustScrollTableRows = function(this) local newRows = math.floor(( this.frmMeasureDummy:GetHeight() - 5 ) / 15); - newRows = (newRows < 3 and 3) or newRows; + newRows = (newRows < 4 and 4) or newRows; this.scrollTable:SetDisplayRows(newRows, 15); end; @@ -275,3 +220,23 @@ InventoriumItemMover:Show(); end + +function addon:SetFrameSettings(title, description, proceed, cancel, headers) + local frame = InventoriumItemMover; + + frame.lblTitle:SetText(title); + + frame.lblDescription:SetText(description); + + frame.btnMove:SetText(proceed.text); + frame.btnMove.tooltipTitle = proceed.tooltipTitle; + frame.btnMove.tooltip = proceed.tooltip; + frame.btnMove.OnClick = proceed.onClick; + + frame.btnCancel:SetText(cancel.text); + frame.btnCancel.tooltipTitle = cancel.tooltipTitle; + frame.btnCancel.tooltip = cancel.tooltip; + frame.btnCancel.OnClick = cancel.onClick; + + frame.scrollTable:SetDisplayCols(headers); +end diff -r 3bbad0429d87 -r 67bd5057ecb7 Modules/Mover.lua --- 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() diff -r 3bbad0429d87 -r 67bd5057ecb7 Modules/Queue.lua --- a/Modules/Queue.lua Wed Jan 12 22:48:25 2011 +0100 +++ b/Modules/Queue.lua Fri Jan 14 23:25:05 2011 +0100 @@ -93,7 +93,7 @@ -- Retrieve group settings local restockTarget = addon:GetOptionByKey(groupName, "restockTarget"); local bonusQueue = addon:GetOptionByKey(groupName, "bonusQueue"); - local minCraftingQueue = floor( addon:GetOptionByKey(groupName, "minCraftingQueue") * restockTarget ); + local minCraftingQueue = floor( addon:GetOptionByKey(groupName, "minCraftingQueue") * restockTarget ); -- If the minCraftingQueue is 5% and restockTarget is 60, this will result in 3 -- Calculate the amount to be queued local amount = ( restockTarget - currentStock ); diff -r 3bbad0429d87 -r 67bd5057ecb7 Modules/Scanner.lua --- a/Modules/Scanner.lua Wed Jan 12 22:48:25 2011 +0100 +++ b/Modules/Scanner.lua Fri Jan 14 23:25:05 2011 +0100 @@ -1,30 +1,107 @@ local addon = select(2, ...); local mod = addon:NewModule("Scanner", "AceEvent-3.0", "AceTimer-3.0"); -addon.Locations = { - Bag = 0, - Bank = 1, - Guild = 2, - Mailbox = 3, -}; - local Mover, paused, currentLocation; local itemCache = {}; -local function OnMoveAccept(this) +local function OnMoveAccept() mod:Pause(); Mover:BeginMove(currentLocation, mod.Unpause); InventoriumItemMover:Hide(); end -local function OnMoveCancel(this) +local function OnMoveCancel() Mover:ResetQueue(); currentLocation = nil; InventoriumItemMover:Hide(); end +local function UseStorageRefillST(withPrices) + local frame = InventoriumItemMover; -- both for speed as code-consistency + + -- Scrolling table with a list of items to be moved + local scrollTableWidth = ( frame.frmMeasureDummy:GetWidth() - 30 ); -- adjust width by the scrollbar size + local headers = { + { + ["name"] = "Item", + ["width"] = (scrollTableWidth * ((withPrices and .5) or .60)), + ["defaultsort"] = "asc", + ["comparesort"] = function(this, aRow, bRow, column) + local aName, _, aRarity = GetItemInfo(this:GetRow(aRow).rowData.itemId); + local bName, _, bRarity = GetItemInfo(this:GetRow(bRow).rowData.itemId); + local template = "%d%s"; + aName = template:format((10 - (aRarity or 10)), (aName or ""):lower()); + bName = template:format((10 - (bRarity or 10)), (bName or ""):lower()); + + if this.cols[column].sort == "dsc" then + return aName > bName; + else + return aName < bName; + end + end, + ["sort"] = "asc", -- when the data is set, use this column so sort the default data + ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"), + ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by item quality then item name."), + }, + { + ["name"] = "Moving", + ["width"] = (scrollTableWidth * .15), + ["align"] = "RIGHT", + ["defaultsort"] = "dsc", + ["sortnext"] = 1, + ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Moving"), + ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the amount of movable items."), + }, + { + ["name"] = "Available", + ["width"] = (scrollTableWidth * ((withPrices and .2) or .25)), + ["align"] = "RIGHT", + ["defaultsort"] = "dsc", + ["sortnext"] = 1, + ["comparesort"] = function(this, aRow, bRow, column) + local aAvailablePercent = (this:GetRow(aRow).rowData.available / this:GetRow(aRow).rowData.missing); + local bAvailablePercent = (this:GetRow(bRow).rowData.available / this:GetRow(bRow).rowData.missing); + + if this.cols[column].sort == "dsc" then + return aAvailablePercent > bAvailablePercent; + else + return aAvailablePercent < bAvailablePercent; + end + end, + ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"), + ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the availibility percentage."), + }, + }; + if withPrices then + table.insert(headers, { + ["name"] = "Price", + ["width"] = (scrollTableWidth * .15), + ["align"] = "RIGHT", + ["defaultsort"] = "dsc", + ["sortnext"] = 1, + ["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Price"), + ["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the price of this item at this vendor."), + }); + end + + local proceedButton = { + text = "Move Items", + tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Move Items"), + tooltip = (not addon.db.profile.defaults.hideHelp and "Start moving these items from the bank."), + onClick = OnMoveAccept, + }; + local cancelButton = { + text = "Cancel", + tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Cancel"), + tooltip = (not addon.db.profile.defaults.hideHelp and "Do not move anything and close the window."), + onClick = OnMoveCancel, + }; + + addon:SetFrameSettings("Inventorium Bank Refill", "The items listed below can be refilled from this location, do you wish to move them to your bags?", proceedButton, cancelButton, headers); +end + function mod:ClearCache() table.wipe(itemCache); end @@ -76,9 +153,9 @@ end elseif location == addon.Locations.Guild then for tabId = 1, GetNumGuildBankTabs() do - local isViewable = select(3, GetGuildBankTabInfo(tabId)); + local _, _, isViewable, _, _, remainingWithdrawals = GetGuildBankTabInfo(tabId); - if isViewable == 1 then + if isViewable and (remainingWithdrawals > 0 or remainingWithdrawals == -1) then local slotId = (MAX_GUILDBANK_SLOTS_PER_TAB or 98); -- start by scanning the last slot while slotId ~= 0 do @@ -108,6 +185,54 @@ end end end + elseif location == addon.Locations.Mailbox then + for mailIndex = 1, GetInboxNumItems() do + -- All mail items + + for attachIndex = 1, ATTACHMENTS_MAX_RECEIVE do + -- All attachments + + local itemLink = GetInboxItemLink(mailIndex, attachIndex); + local itemId = itemLink and addon:GetItemId(itemLink); + local itemCount = itemLink and select(3, GetInboxItem(mailIndex, attachIndex)); + + if itemLink and itemId and itemCount and itemCount > 0 then + local itemMove; + if not itemCache[itemId] then + -- If this is the first time we see this item, make a new object + itemMove = addon.ContainerItem:New(); + itemCache[itemId] = itemMove; + else + -- If we had this item in another slot too + itemMove = itemCache[itemId]; + end + + itemMove:AddLocation(mailIndex, attachIndex, itemCount); + end + end + end + elseif location == addon.Locations.Merchant then + for itemIndex = 1, GetMerchantNumItems() do + -- All merchant items + + local itemLink = GetMerchantItemLink(itemIndex); + local itemId = itemLink and addon:GetItemId(itemLink); + local _, _, vendorValue, _, numAvailable, _, extendedCost = GetMerchantItemInfo(index); + + if itemLink and itemId and numAvailable ~= 0 and not extendedCost then + local itemMove; + if not itemCache[itemId] then + -- If this is the first time we see this item, make a new object + itemMove = addon.ContainerItem:New(); + itemCache[itemId] = itemMove; + else + -- If we had this item in another slot too + itemMove = itemCache[itemId]; + end + + itemMove:AddLocation(1, itemIndex, numAvailable, vendorValue); + end + end else error("Invalid location provided for CacheLocation. Must be Bank or Guild."); end @@ -125,6 +250,7 @@ function mod:Scan(location) -- We might pause the scanning when we invoke moves ourself if paused then + addon:Debug("Not scanning; paused..."); return; end @@ -133,6 +259,9 @@ currentLocation = location; self:CacheLocation(location, true); + -- Ensure previous queue isn't remaining + Mover:ResetQueue(); + -- Go through all groups for groupName, values in pairs(addon.db.profile.groups) do local trackAt = addon:GetOptionByKey(groupName, "trackAtCharacters"); @@ -153,14 +282,12 @@ -- Check how many are available local availableItems = ((itemCache[itemId] and itemCache[itemId].totalCount) or 0); -- Calculate how many we'll be moving (less missing than available? use missing, otherwise use available) - local moving = (((missingItems <= availableItems) and missingItems) or availableItems); + local moving = (((availableItems == -1 or missingItems <= availableItems) and missingItems) or availableItems); - if availableItems > 0 then - --addon:Print("Insufficient " .. IdToItemLink(itemId) .. " but this location has " .. availableItems .. " (moving " .. moving .. ")"); + if availableItems ~= 0 then + addon:Debug("Insufficient %s but this location has %s (moving %d)", IdToItemLink(itemId), ((availableItems == -1 and "unlimited") or availableItems), moving); - Mover:AddMove(itemId, moving, missingItems, availableItems); - else - --addon:Print("Insufficient " .. IdToItemLink(itemId)); + Mover:AddMove(itemId, moving, missingItems, availableItems, itemCache[itemId]:GetVendorPrice()); end end end @@ -171,29 +298,38 @@ if Mover:HasMoves() then if addon.db.profile.defaults.autoRefillSkipConfirm then - OnMoveAccept(true); + OnMoveAccept(); else + UseStorageRefillST((location == addon.Locations.Merchant)); + -- This table is never copied, just referenced. It is the same for every row. local columns = { { - value = function(data, cols, realrow, column, table) - return IdToItemLink(data[realrow].rowData.id); + ["value"] = function(data, cols, realrow, column, table) + return IdToItemLink(data[realrow].rowData.itemId); end, }, -- item { - value = function(data, cols, realrow, column, table) + ["value"] = function(data, cols, realrow, column, table) return data[realrow].rowData.num; end, }, -- moving { - value = function(data, cols, realrow, column, table) + ["value"] = function(data, cols, realrow, column, table) return addon:DisplayItemCount(data[realrow].rowData.available, data[realrow].rowData.missing); -- available / missing end, - color = function(data, cols, realrow, column, table) + ["color"] = function(data, cols, realrow, column, table) return ((data[realrow].rowData.available < data[realrow].rowData.missing) and { r = 1, g = 0, b = 0, a = 1 }) or { r = 1, g = 1, b = 1, a = 1 }; end, }, -- missing / available }; + if location == addon.Locations.Merchant then + table.insert(columns, { + ["value"] = function(data, cols, realrow, column, table) + return GetCoinTextureString(data[realrow].rowData.price * data[realrow].rowData.num); + end, + }); + end -- Store the list with rows in this local data = {}; @@ -279,8 +415,9 @@ -- Get the contents for every tab into our cache for tabId = 1, GetNumGuildBankTabs() do - local isViewable = select(3, GetGuildBankTabInfo(tabId)); - if isViewable == 1 then + local _, _, isViewable, _, _, remainingWithdrawals = GetGuildBankTabInfo(tabId); + + if isViewable and (remainingWithdrawals > 0 or remainingWithdrawals == -1) then QueryGuildBankTab(tabId); end end @@ -289,10 +426,79 @@ self:RegisterEvent("GUILDBANKBAGSLOTS_CHANGED"); end +function mod:MERCHANT_SHOW() + addon:Debug("Scanner:MERCHANT_SHOW"); + + self:RegisterEvent("MERCHANT_CLOSED"); + + self:Scan(addon.Locations.Merchant); +end + +function mod:MERCHANT_CLOSED() + addon:Debug("Scanner:MERCHANT_CLOSED"); + + self:ClearCache(); + + self:UnregisterEvent("MERCHANT_CLOSED"); + + InventoriumItemMover:Hide(); + Mover:ResetQueue(); +end + +--local previousMailCount; +--function mod:MAIL_SHOW() +-- addon:Debug("Scanner:MAIL_SHOW"); +-- +-- self:RegisterEvent("MAIL_INBOX_UPDATE"); +-- self:RegisterEvent("MAIL_CLOSED"); +-- +-- scanned = nil; +-- previousMailCount = nil; +-- +-- self:Scan(addon.Locations.Mailbox); +--end + +--function mod:MAIL_INBOX_UPDATE() +-- if not scanned then +-- addon:Debug("Scanner:MAIL_INBOX_UPDATE"); +-- +-- local current, total = GetInboxNumItems(); +-- +-- if not previousMailCount or current > previousMailCount then +-- -- New mail received +-- +-- scanned = true; +-- +-- self:Scan(addon.Locations.Mailbox); +-- end +-- +-- -- Also remember the new mailcount when losing items, otherwise deleting item 50 and getting to 50 again wouldn't trigger a re-scan +-- previousMailCount = current; +-- else +-- addon:Debug("Scanner:MAIL_INBOX_UPDATE skipped, already scanned"); +-- end +--end + +--function mod:MAIL_CLOSED() +-- addon:Debug("Scanner:MAIL_CLOSED"); +-- +-- previousMailCount = nil; +-- scanned = nil; +-- self:ClearCache(); +-- +-- self:UnregisterEvent("MAIL_INBOX_UPDATE"); +-- self:UnregisterEvent("MAIL_CLOSED"); +-- +-- InventoriumItemMover:Hide(); +-- Mover:ResetQueue(); +--end + function mod:OnEnable() -- Scan once when the bankframe is opened self:RegisterEvent("BANKFRAME_OPENED"); self:RegisterEvent("GUILDBANKFRAME_OPENED"); +-- self:RegisterEvent("MAIL_SHOW"); + self:RegisterEvent("MERCHANT_SHOW"); Mover = addon:GetModule("Mover"); @@ -307,11 +513,16 @@ paused = nil; -- Bank + self:BANKFRAME_CLOSED(); self:UnregisterEvent("BANKFRAME_OPENED"); -- Guild self:GUILDBANKFRAME_CLOSED(); self:UnregisterEvent("GUILDBANKFRAME_OPENED"); + +-- -- Mailbox +-- self:MAIL_CLOSED(); +-- self:UnregisterEvent("MAIL_SHOW"); end function mod:Pause()