annotate Mover.lua @ 82:f885805da5d6

Added options to toggle the automatic refilling. This defaults to true. Normalized property amount names; a move has a ?num? that must be moved and a location has a ?count? indicating the amount of items at that slot. Target/source item verification should now be working properly for guilds. When ?bank? is included in the local item count, we will skip trying to auto refill from this.
author Zerotorescue
date Thu, 06 Jan 2011 10:48:56 +0100
parents 58617c7827fa
children
rev   line source
Zerotorescue@80 1 local addon = select(2, ...);
Zerotorescue@80 2 local mod = addon:NewModule("Mover", "AceEvent-3.0", "AceTimer-3.0");
Zerotorescue@80 3
Zerotorescue@80 4 local Scanner;
Zerotorescue@80 5 local queuedMoves = {}; -- table storing all queued moves before BeginMove is called
Zerotorescue@80 6 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 #)
Zerotorescue@81 7 local movesSource;
Zerotorescue@80 8
Zerotorescue@80 9 function mod:AddMove(itemId, amount)
Zerotorescue@80 10 table.insert(queuedMoves, {
Zerotorescue@80 11 id = itemId,
Zerotorescue@80 12 num = amount,
Zerotorescue@80 13 });
Zerotorescue@80 14 end
Zerotorescue@80 15
Zerotorescue@81 16 function mod:HasMoves()
Zerotorescue@81 17 return (#queuedMoves ~= 0);
Zerotorescue@81 18 end
Zerotorescue@81 19
Zerotorescue@80 20 function mod:BeginMove(location, onFinish)
Zerotorescue@81 21 addon:Debug("BeginMove");
Zerotorescue@80 22
Zerotorescue@80 23 -- Find the outgoing moves
Zerotorescue@80 24 -- 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
Zerotorescue@80 25
Zerotorescue@80 26 -- Get a list of items in the source container
Zerotorescue@80 27 local sourceContents = Scanner:CacheLocation(location, false);
Zerotorescue@80 28
Zerotorescue@80 29 local outgoingMoves = {};
Zerotorescue@80 30
Zerotorescue@82 31 addon:Debug(#queuedMoves .. " moves were queued.");
Zerotorescue@82 32
Zerotorescue@81 33 for _, singleMove in pairs(queuedMoves) do
Zerotorescue@80 34 local sourceItem = sourceContents[singleMove.id];
Zerotorescue@80 35 if not sourceItem then
Zerotorescue@80 36 print("Can't move " .. IdToItemLink(singleMove.id) .. ", non-existant in source");
Zerotorescue@80 37 else
Zerotorescue@82 38 -- 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)
Zerotorescue@80 39 table.sort(sourceItem.locations, function(a, b)
Zerotorescue@81 40 return a.count < b.count;
Zerotorescue@80 41 end);
Zerotorescue@80 42
Zerotorescue@81 43 for _, itemLocation in pairs(sourceItem.locations) do
Zerotorescue@80 44 -- if this location has more items than we need, only move what we need, otherwise move everything in this stack
Zerotorescue@80 45 local movingNum = ((itemLocation.count > singleMove.num and singleMove.num) or itemLocation.count);
Zerotorescue@80 46
Zerotorescue@80 47 table.insert(outgoingMoves, {
Zerotorescue@80 48 itemId = singleMove.id,
Zerotorescue@82 49 num = movingNum,
Zerotorescue@80 50 container = itemLocation.container,
Zerotorescue@80 51 slot = itemLocation.slot,
Zerotorescue@80 52 });
Zerotorescue@80 53
Zerotorescue@80 54 singleMove.num = (singleMove.num - movingNum);
Zerotorescue@80 55
Zerotorescue@80 56 if singleMove.num == 0 then
Zerotorescue@80 57 -- If we have prepared everything we wanted, go to the next queued move
Zerotorescue@81 58 break; -- stop the locations-loop
Zerotorescue@80 59 end
Zerotorescue@80 60 end
Zerotorescue@80 61 end
Zerotorescue@80 62 end
Zerotorescue@82 63
Zerotorescue@82 64 addon:Debug(#outgoingMoves .. " outgoing moves are possible.");
Zerotorescue@80 65
Zerotorescue@80 66 -- No longer needed
Zerotorescue@80 67 table.wipe(queuedMoves);
Zerotorescue@80 68
Zerotorescue@80 69 -- Process every single outgoing move and find fitting targets
Zerotorescue@80 70
Zerotorescue@80 71 -- Get a list of items already in the target container
Zerotorescue@80 72 local targetContents = Scanner:CacheLocation(addon.Locations.Bag, false);
Zerotorescue@80 73
Zerotorescue@80 74 -- Find all empty slots
Zerotorescue@80 75
Zerotorescue@80 76 local emptySlots = {};
Zerotorescue@80 77
Zerotorescue@80 78 local start = 0;
Zerotorescue@80 79 local stop = NUM_BAG_SLOTS;
Zerotorescue@80 80
Zerotorescue@80 81 -- Go through all our bags, including the backpack
Zerotorescue@80 82 for bagId = start, stop do
Zerotorescue@80 83 -- Go through all our slots
Zerotorescue@80 84 for slotId = 1, GetContainerNumSlots(bagId) do
Zerotorescue@82 85 local itemId = GetContainerItemID(bagId, slotId); -- we're scanning our local bags here, so no need to get messy with guild bank support
Zerotorescue@80 86
Zerotorescue@80 87 if not itemId then
Zerotorescue@80 88 table.insert(emptySlots, {
Zerotorescue@81 89 container = bagId,
Zerotorescue@81 90 slot = slotId,
Zerotorescue@80 91 });
Zerotorescue@80 92 end
Zerotorescue@80 93 end
Zerotorescue@80 94 end
Zerotorescue@82 95
Zerotorescue@82 96 addon:Debug(#emptySlots .. " empty slots are available.");
Zerotorescue@80 97
Zerotorescue@81 98 -- Remember where we're moving from
Zerotorescue@81 99 movesSource = location;
Zerotorescue@81 100
Zerotorescue@80 101 while #outgoingMoves ~= 0 do
Zerotorescue@80 102 -- A not equal-comparison should be quicker than a larger/smaller than-comparison
Zerotorescue@80 103
Zerotorescue@81 104 for _, outgoingMove in pairs(outgoingMoves) do
Zerotorescue@80 105 -- itemId will be set to nil when this outgoing move was processed - sanity check
Zerotorescue@80 106 if outgoingMove.itemId then
Zerotorescue@80 107 local targetItem = targetContents[outgoingMove.itemId];
Zerotorescue@80 108
Zerotorescue@80 109 if not targetItem then
Zerotorescue@80 110 -- grab an empty slot
Zerotorescue@80 111 -- make new instance of ItemMove
Zerotorescue@80 112 -- populate targetContents with it so future moves of this item can be put on top of it if this isn't a full stack
Zerotorescue@80 113
Zerotorescue@80 114 local firstAvailableSlot = emptySlots[1];
Zerotorescue@80 115
Zerotorescue@80 116 if not firstAvailableSlot then
Zerotorescue@80 117 print("Bags are full. Skipping " .. IdToItemLink(outgoingMove.itemId) .. ".");
Zerotorescue@80 118
Zerotorescue@82 119 outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
Zerotorescue@80 120 else
Zerotorescue@80 121 table.insert(combinedMoves, {
Zerotorescue@82 122 itemId = outgoingMove.itemId,
Zerotorescue@82 123 num = outgoingMove.num,
Zerotorescue@80 124 sourceContainer = outgoingMove.container,
Zerotorescue@80 125 sourceSlot = outgoingMove.slot,
Zerotorescue@80 126 targetContainer = firstAvailableSlot.container,
Zerotorescue@80 127 targetSlot = firstAvailableSlot.slot,
Zerotorescue@80 128 });
Zerotorescue@80 129
Zerotorescue@80 130 -- We filled an empty slot so the target contents now has one more item,
Zerotorescue@80 131 -- make a new instance of the ItemMove class so any additional items with this id can be stacked on top of it
Zerotorescue@81 132 local itemMove = addon.ContainerItem:New();
Zerotorescue@82 133 itemMove:AddLocation(firstAvailableSlot.container, firstAvailableSlot.slot, outgoingMove.num);
Zerotorescue@80 134 targetContents[outgoingMove.itemId] = itemMove;
Zerotorescue@80 135
Zerotorescue@81 136 table.remove(emptySlots, 1); -- no longer empty
Zerotorescue@80 137
Zerotorescue@82 138 outgoingMove.num = 0; -- nothing remaining - sanity check
Zerotorescue@80 139 outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
Zerotorescue@80 140 end
Zerotorescue@80 141 else
Zerotorescue@80 142 -- Find the maximum stack size for this item
Zerotorescue@80 143 local itemStackCount = select(8, GetItemInfo(outgoingMove.itemId));
Zerotorescue@80 144
Zerotorescue@80 145 -- We want to move to the largest stacks first to keep stuff pretty
Zerotorescue@80 146 table.sort(targetItem.locations, function(a, b)
Zerotorescue@81 147 return a.count > b.count;
Zerotorescue@80 148 end);
Zerotorescue@80 149
Zerotorescue@81 150 for _, itemLocation in pairs(targetItem.locations) do
Zerotorescue@82 151 if itemLocation.count < itemStackCount and outgoingMove.num > 0 then
Zerotorescue@80 152 -- Check if this stack isn't already full (and we still need to move this item)
Zerotorescue@80 153
Zerotorescue@80 154 local remainingSpace = (itemStackCount - itemLocation.count);
Zerotorescue@82 155 if remainingSpace > outgoingMove.num then
Zerotorescue@80 156 -- Enough room to move this entire stack
Zerotorescue@80 157 -- Deposit this item and then forget this outgoing move as everything in it was processed
Zerotorescue@80 158
Zerotorescue@80 159 table.insert(combinedMoves, {
Zerotorescue@82 160 itemId = outgoingMove.itemId,
Zerotorescue@82 161 num = outgoingMove.num,
Zerotorescue@80 162 sourceContainer = outgoingMove.container,
Zerotorescue@80 163 sourceSlot = outgoingMove.slot,
Zerotorescue@80 164 targetContainer = itemLocation.container,
Zerotorescue@80 165 targetSlot = itemLocation.slot,
Zerotorescue@80 166 });
Zerotorescue@80 167
Zerotorescue@82 168 itemLocation.count = (itemLocation.count + outgoingMove.num);
Zerotorescue@80 169 outgoingMove.count = 0; -- nothing remaining
Zerotorescue@80 170 outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
Zerotorescue@80 171 break; -- stop the locations-loop
Zerotorescue@80 172 else
Zerotorescue@80 173 -- Deposit this item but don't remove the outgoing move as there are some items left to move
Zerotorescue@80 174
Zerotorescue@80 175 table.insert(combinedMoves, {
Zerotorescue@82 176 itemId = outgoingMove.itemId,
Zerotorescue@82 177 num = outgoingMove.num,
Zerotorescue@80 178 sourceContainer = outgoingMove.container,
Zerotorescue@80 179 sourceSlot = outgoingMove.slot,
Zerotorescue@80 180 targetContainer = itemLocation.container,
Zerotorescue@80 181 targetSlot = itemLocation.slot,
Zerotorescue@80 182 });
Zerotorescue@80 183
Zerotorescue@80 184 -- The target will be full when we complete, but the source will still have remaining items left to be moved
Zerotorescue@80 185 itemLocation.count = itemStackCount;
Zerotorescue@82 186 outgoingMove.num = (outgoingMove.num - remainingSpace);
Zerotorescue@80 187 end
Zerotorescue@80 188 end
Zerotorescue@80 189 end
Zerotorescue@80 190
Zerotorescue@82 191 if outgoingMove.num > 0 then
Zerotorescue@80 192 -- We went through all matching items and checked their stack sizes if we could move this there, no room available
Zerotorescue@80 193 -- 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
Zerotorescue@80 194 targetItem = nil;
Zerotorescue@80 195 end
Zerotorescue@80 196 end
Zerotorescue@80 197 end
Zerotorescue@80 198 end
Zerotorescue@80 199
Zerotorescue@80 200 -- Loop through the array to find items that should be removed, start with the last element or the loop would break
Zerotorescue@80 201 local numOutgoingMoves = #outgoingMoves; -- since LUA-tables start at an index of 1, this is actually an existing index (outgoingMoves[#outgoingMoves] would return a value)
Zerotorescue@80 202 while numOutgoingMoves ~= 0 do
Zerotorescue@80 203 -- A not equal-comparison should be quicker than a larger/smaller than-comparison
Zerotorescue@80 204
Zerotorescue@80 205 -- Check if the item id is nil, this is set to nil when this outgoing move has been processed
Zerotorescue@80 206 if not outgoingMoves[numOutgoingMoves].itemId then
Zerotorescue@80 207 -- Remove this element from the array
Zerotorescue@80 208 table.remove(outgoingMoves, numOutgoingMoves);
Zerotorescue@80 209 end
Zerotorescue@80 210
Zerotorescue@80 211 -- Proceed with the next element (or previous considering we're going from last to first)
Zerotorescue@80 212 numOutgoingMoves = (numOutgoingMoves - 1);
Zerotorescue@80 213 end
Zerotorescue@80 214 end
Zerotorescue@82 215
Zerotorescue@82 216 addon:Debug(#combinedMoves .. " moves should be possible.");
Zerotorescue@80 217
Zerotorescue@80 218 -- No longer needed
Zerotorescue@80 219 table.wipe(emptySlots);
Zerotorescue@80 220
Zerotorescue@81 221 self:ProcessMove();
Zerotorescue@80 222
Zerotorescue@82 223 -- Even though we aren't completely done yet, allow requeueing
Zerotorescue@81 224 onFinish();
Zerotorescue@80 225 end
Zerotorescue@80 226
Zerotorescue@81 227 if not table.reverse then
Zerotorescue@81 228 -- table.reverse = function(orig)
Zerotorescue@81 229 -- local temp = CopyTable(orig);
Zerotorescue@81 230 -- local origLength = #temp;
Zerotorescue@81 231 -- for i = 1, origLength do
Zerotorescue@81 232 -- orig[(origLength - i + 1)] = temp[i];
Zerotorescue@81 233 -- end
Zerotorescue@81 234 -- end
Zerotorescue@81 235 table.reverse = function(orig)
Zerotorescue@81 236 local temp = {};
Zerotorescue@81 237 local origLength = #orig;
Zerotorescue@81 238 for i = 1, origLength do
Zerotorescue@81 239 temp[(origLength - i + 1)] = orig[i];
Zerotorescue@81 240 end
Zerotorescue@81 241
Zerotorescue@81 242 -- -- Update the original table (can't do orig = temp as that would change the reference-link instead of the original table)
Zerotorescue@81 243 -- for i, v in pairs(temp) do
Zerotorescue@81 244 -- orig[i] = v;
Zerotorescue@81 245 -- end
Zerotorescue@81 246 return temp; -- for speed we choose to do a return instead
Zerotorescue@81 247 end
Zerotorescue@81 248 end
Zerotorescue@81 249
Zerotorescue@81 250 function mod:ProcessMove()
Zerotorescue@81 251 addon:Debug("ProcessMove");
Zerotorescue@81 252
Zerotorescue@81 253 if #combinedMoves == 0 then
Zerotorescue@81 254 print("Nothing to move.");
Zerotorescue@81 255
Zerotorescue@81 256 self:Abort();
Zerotorescue@81 257
Zerotorescue@81 258 return;
Zerotorescue@81 259 end
Zerotorescue@81 260
Zerotorescue@82 261 self:RegisterEvent("BAG_UPDATE");
Zerotorescue@81 262 self:RegisterEvent("UI_ERROR_MESSAGE");
Zerotorescue@81 263
Zerotorescue@80 264 -- combinedMoves now has all moves in it (source -> target)
Zerotorescue@80 265 -- go through list, move everything inside it
Zerotorescue@81 266 -- add source and target to lists, if either is already in this list, skip the move
Zerotorescue@81 267 -- repeat every few seconds until we're completely done
Zerotorescue@80 268
Zerotorescue@80 269 local sourceLocationsLocked = {};
Zerotorescue@80 270 local targetLocationsLocked = {};
Zerotorescue@80 271
Zerotorescue@81 272 -- 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
Zerotorescue@82 273 combinedMoves = table.reverse(combinedMoves);
Zerotorescue@81 274
Zerotorescue@82 275 local GetContainerItemID;
Zerotorescue@82 276 if movesSource == addon.Locations.Guild then
Zerotorescue@82 277 GetContainerItemID = GetGuildBankItemLink;
Zerotorescue@82 278 else
Zerotorescue@82 279 GetContainerItemID = GetContainerItemID;
Zerotorescue@82 280 end
Zerotorescue@82 281
Zerotorescue@82 282 local combinedMovesOriginalLength = #combinedMoves;
Zerotorescue@82 283 local numCurrentMove = combinedMovesOriginalLength;
Zerotorescue@80 284 while numCurrentMove ~= 0 do
Zerotorescue@80 285 local move = combinedMoves[numCurrentMove];
Zerotorescue@80 286
Zerotorescue@80 287 -- sourceContainer, sourceSlot, targetContainer, targetSlot, itemId, num
Zerotorescue@81 288 if move and (not sourceLocationsLocked[move.sourceContainer] or not sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) and
Zerotorescue@80 289 (not targetLocationsLocked[move.targetContainer] or not targetLocationsLocked[move.targetContainer][move.targetSlot]) then
Zerotorescue@80 290
Zerotorescue@80 291 print("Moving " .. IdToItemLink(move.itemId));
Zerotorescue@80 292
Zerotorescue@81 293 addon:Debug(("Moving %dx%s from (%d,%d) to (%d,%d)"):format(move.num, IdToItemLink(move.itemId), move.sourceContainer, move.sourceSlot, move.targetContainer, move.targetSlot));
Zerotorescue@81 294
Zerotorescue@81 295 if GetContainerItemID(move.sourceContainer, move.sourceSlot) ~= move.itemId then
Zerotorescue@81 296 self:Abort("source changed", "Source (" .. move.sourceContainer .. "," .. move.sourceSlot .. ") is not " .. IdToItemLink(move.itemId));
Zerotorescue@81 297 return;
Zerotorescue@81 298 end
Zerotorescue@81 299
Zerotorescue@80 300 -- Pickup stack
Zerotorescue@81 301 if movesSource == addon.Locations.Bank then
Zerotorescue@81 302 SplitContainerItem(move.sourceContainer, move.sourceSlot, move.num);
Zerotorescue@81 303 elseif movesSource == addon.Locations.Guild then
Zerotorescue@81 304 SplitGuildBankItem(move.sourceContainer, move.sourceSlot, move.num);
Zerotorescue@81 305 end
Zerotorescue@80 306
Zerotorescue@80 307 -- Remember we picked this item up and thus it is now locked
Zerotorescue@80 308 if not sourceLocationsLocked[move.sourceContainer] then
Zerotorescue@80 309 sourceLocationsLocked[move.sourceContainer] = {};
Zerotorescue@80 310 end
Zerotorescue@80 311 sourceLocationsLocked[move.sourceContainer][move.sourceSlot] = true;
Zerotorescue@80 312
Zerotorescue@81 313 if movesSource == addon.Locations.Guild or CursorHasItem() then -- CursorHasItem is always false if source is a guild tab
Zerotorescue@81 314 if GetContainerItemID(move.targetContainer, move.targetSlot) and GetContainerItemID(move.targetContainer, move.targetSlot) ~= move.itemId then
Zerotorescue@81 315 self:Abort("target changed", "Target (" .. move.targetContainer .. "," .. move.targetSlot .. ") is not " .. IdToItemLink(move.itemId) .. " nor empty");
Zerotorescue@81 316 return;
Zerotorescue@81 317 end
Zerotorescue@81 318
Zerotorescue@82 319 -- And drop it (this is always a local bag so no need to do any guild-checks)
Zerotorescue@80 320 PickupContainerItem(move.targetContainer, move.targetSlot);
Zerotorescue@80 321
Zerotorescue@80 322 -- Remember we dropped an item here and thus this is now locked
Zerotorescue@81 323 if not targetLocationsLocked[move.targetContainer] then
Zerotorescue@81 324 targetLocationsLocked[move.targetContainer] = {};
Zerotorescue@80 325 end
Zerotorescue@81 326 targetLocationsLocked[move.targetContainer][move.targetSlot] = true;
Zerotorescue@80 327
Zerotorescue@80 328 -- This move was processed
Zerotorescue@80 329 table.remove(combinedMoves, numCurrentMove);
Zerotorescue@81 330 else
Zerotorescue@81 331 self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false");
Zerotorescue@81 332 return;
Zerotorescue@80 333 end
Zerotorescue@80 334 end
Zerotorescue@80 335
Zerotorescue@80 336 -- Proceed with the next element (or previous considering we're going from last to first)
Zerotorescue@80 337 numCurrentMove = (numCurrentMove - 1);
Zerotorescue@80 338 end
Zerotorescue@81 339
Zerotorescue@82 340 addon:Debug((combinedMovesOriginalLength - #combinedMoves) .. " moves processed. " .. #combinedMoves .. " moves remaining.");
Zerotorescue@82 341
Zerotorescue@81 342 if #combinedMoves == 0 then
Zerotorescue@81 343 print("Finished.");
Zerotorescue@81 344
Zerotorescue@81 345 self:Abort();
Zerotorescue@81 346
Zerotorescue@81 347 return;
Zerotorescue@81 348 end
Zerotorescue@80 349 end
Zerotorescue@80 350
Zerotorescue@80 351 local tmrProcessNext;
Zerotorescue@81 352 function mod:BAG_UPDATE()
Zerotorescue@81 353 self:CancelTimer(tmrProcessNext, true); -- silent
Zerotorescue@81 354 tmrProcessNext = self:ScheduleTimer("ProcessMove", 1);
Zerotorescue@80 355 end
Zerotorescue@80 356
Zerotorescue@81 357 function IdToItemLink(itemId)
Zerotorescue@81 358 local itemLink = select(2, GetItemInfo(itemId));
Zerotorescue@81 359 itemLink = itemLink or "Unknown (" .. itemId .. ")";
Zerotorescue@81 360 return itemLink;
Zerotorescue@81 361 end
Zerotorescue@81 362
Zerotorescue@81 363 function mod:UI_ERROR_MESSAGE(e, errorMessage)
Zerotorescue@81 364 if errorMessage == ERR_SPLIT_FAILED then
Zerotorescue@81 365 self:Abort("splitting failed", "Splitting failed.");
Zerotorescue@80 366 end
Zerotorescue@80 367 end
Zerotorescue@80 368
Zerotorescue@81 369 function mod:Abort(simple, debugMsg)
Zerotorescue@81 370 if debugMsg then
Zerotorescue@81 371 addon:Debug("Aborting:" .. debugMsg);
Zerotorescue@81 372 end
Zerotorescue@81 373 if simple then
Zerotorescue@81 374 print("|cffff0000Aborting: " .. simple .. ".|r");
Zerotorescue@81 375 end
Zerotorescue@81 376 table.wipe(combinedMoves);
Zerotorescue@81 377 movesSource = nil;
Zerotorescue@80 378
Zerotorescue@81 379 -- Stop timer
Zerotorescue@81 380 self:UnregisterEvent("BAG_UPDATE");
Zerotorescue@81 381 self:CancelTimer(tmrProcessNext, true); -- silent
Zerotorescue@80 382
Zerotorescue@81 383 self:UnregisterEvent("UI_ERROR_MESSAGE");
Zerotorescue@80 384 end
Zerotorescue@80 385
Zerotorescue@80 386 function mod:OnEnable()
Zerotorescue@80 387 Scanner = addon:GetModule("Scanner");
Zerotorescue@80 388 end
Zerotorescue@80 389
Zerotorescue@80 390 function mod:OnDisable()
Zerotorescue@80 391 Scanner = nil;
Zerotorescue@81 392
Zerotorescue@81 393 self:Abort();
Zerotorescue@80 394 end