comparison Modules/Mover.lua @ 84:3bec0ea44607

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