Quantcast Adding a delay to the execution of a loop - WoWInterface
Thread Tools Display Modes
02-23-18, 03:36 AM   #1
Eommus
A Fallenroot Satyr
Join Date: Apr 2017
Posts: 28
Adding a delay to the execution of a loop

Hi,

I use the following code to sell all items in a given bag, which is triggered when I click a Sell button for that bag.

Code:
function SellAllInBag(bag)
	for slot = 1, GetContainerNumSlots(bag) do
		UseContainerItem(bag, slot)
	end
end
The above loop tries to sell all the items in the bag at the same time, so it gets stuck and I have to click the Sell button 2-3 times to sell all items in a large bag.

What I am looking for is to add some sort of a delay (e.g. 0.1 seconds) to the loop, so that it will sell all items with one click without getting stuck.

Something like:

Code:
function SellAllInBag(bag)
	for slot = 1, GetContainerNumSlots(bag) do
		UseContainerItem(bag, slot)
		-- wait for 0.1 seconds, then continue with the loop
	end
end
Is that possible in LUA coding?

Thanks.
  Reply With Quote
02-23-18, 04:28 AM   #2
zork
A Pyroguard Emberseer
 
zork's Avatar
AddOn Author - Click to view addons
Join Date: Jul 2008
Posts: 1,686
I use C_Timer.After for that.
https://github.com/zorker/rothui/blo.../core.lua#L118
__________________
| Simple is beautiful.
| WoWI AddOns | GitHub | Zork (WoW) | TDMOG

"I wonder what the non-pathetic people are doing tonight?" - Rajesh Koothrappali (The Big Bang Theory)
  Reply With Quote
02-23-18, 06:30 AM   #3
kurapica.igas
A Black Drake
Join Date: Aug 2011
Posts: 89
I have post a new framework named Scorpio, it'd provide a code style like :

Lua Code:
  1. __Thread__()
  2. function SellAllInBag(bag)
  3.     for slot = 1, GetContainerNumSlots(bag) do
  4.         UseContainerItem(bag, slot)
  5.         Delay(0.1)
  6.     end
  7. end

But you couldn't use a framework only for one feature, so there is another way :

Lua Code:
  1. local function delay(tick)
  2.     local th = coroutine.running()
  3.     C_Timer.After(tick, function() coroutine.resume(th) end)
  4.     coroutine.yield()
  5. end
  6.  
  7. function SellAllInBag(bag)
  8.     for slot = 1, GetContainerNumSlots(bag) do
  9.         UseContainerItem(bag, slot)
  10.         delay(0.1)
  11.     end
  12. end
  13.  
  14. -- Call the function as coroutine
  15. coroutine.wrap(SellAllInBag)(0)

Since the code is simple, you can use zork's solution, but it's better to use the coroutine when it's too hard to track the process states.

Last edited by kurapica.igas : 02-23-18 at 06:32 AM.
  Reply With Quote
02-23-18, 08:25 PM   #4
Kanegasi
A Cobalt Mageweaver
 
Kanegasi's Avatar
AddOn Author - Click to view addons
Join Date: Apr 2007
Posts: 232
Here's my take on using C_Timer.After for this:

Lua Code:
  1. function SellAllInBag(bag)
  2.     for slot = 1, GetContainerNumSlots(bag) do
  3.         C_Timer.After(slot/10, UseContainerItem(bag, slot) )
  4.     end
  5. end

I don't know how expensive this is, but it will schedule several timers with increasing delays all at once. The first action will be 0.1 seconds after the function is called, next will be 0.2 seconds, and so on.
  Reply With Quote
02-23-18, 09:47 PM   #5
Ammako
A Cobalt Mageweaver
 
Ammako's Avatar
AddOn Author - Click to view addons
Join Date: Jun 2016
Posts: 228
I know that, 1:1, it is less efficient/more expensive than C_Timer.After, but would C_Timer.NewTicker be fine for this? Or would it still be less efficient than scheduling dozens of C_Timer.After?

Though it looks like NewTicker doesn't let you pass arguments to the function being called, so you'd have to do something silly like using external variables instead.

PoC:
lua Code:
  1. local slot
  2. local currentBag
  3.  
  4. local function SellAllInBag()
  5.     print("Bag number: " .. currentBag) -- debug
  6.     print("Item slot being sold: " .. slot) -- debug
  7.     UseContainerItem(currentBag, slot)
  8.     slot = slot + 1
  9. end
  10.  
  11. local function DoThing(bag)
  12.     slot = 1
  13.     currentBag = bag
  14.     local timer = C_Timer.NewTicker(0.1, SellAllInBag, GetContainerNumSlots(bag))
  15. end

Works as expected if I run DoThing([bag number]).
__________________
█████████████
█████████████
█████████████
█████████████
  Reply With Quote
02-24-18, 11:45 AM   #6
MunkDev
A Scalebane Royal Guard
 
MunkDev's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2015
Posts: 402
Lua Code:
  1. SellAllInBag = CreateFrame('Frame')
  2. SellAllInBag.itemsToSell = {}
  3. SellAllInBag.pendingBags = {}
  4.  
  5. function SellAllInBag:SetCurrentBag(bagID)
  6.     wipe(self.itemsToSell)
  7.     self.currentBagID = bagID
  8.     self.itemsSold = 0
  9.  
  10.     local toggleEvent = (bagID ~= nil) and self.RegisterEvent or self.UnregisterEvent
  11.  
  12.     toggleEvent(self, 'BAG_UPDATE')
  13.     toggleEvent(self, 'BAG_UPDATE_DELAYED')
  14. end
  15.  
  16. function SellAllInBag:AttemptSellItems(index)
  17.     for i=index or 1, #self.itemsToSell do
  18.         UseContainerItem(self.currentBagID, self.itemsToSell[i])
  19.     end
  20. end
  21.  
  22. function SellAllInBag:ProcessNextInQueue()
  23.     local bagInQueue = tremove(self.pendingBags, 1)
  24.     if bagInQueue then
  25.         self(bagInQueue)
  26.     end
  27. end
  28.  
  29. function SellAllInBag:OnEvent(event)
  30.     if ( event == 'BAG_UPDATE' ) then
  31.         self.itemsSold = self.itemsSold + 1
  32.     elseif ( event == 'BAG_UPDATE_DELAYED' ) then
  33.         if ( self.itemsSold == #self.itemsToSell ) then
  34.             self:SetCurrentBag(nil)
  35.             self:ProcessNextInQueue()
  36.         else
  37.             self:AttemptSellItems(self.itemsSold)
  38.         end
  39.     elseif ( event == 'MERCHANT_SHOW' ) then
  40.         self.merchantAvailable = true
  41.     elseif ( event == 'MERCHANT_CLOSED' ) then
  42.         self.merchantAvailable = false
  43.         self:SetCurrentBag(nil)
  44.         wipe(self.pendingBags)
  45.     end
  46. end
  47.  
  48. SellAllInBag:RegisterEvent('MERCHANT_SHOW')
  49. SellAllInBag:RegisterEvent('MERCHANT_CLOSED')
  50. SellAllInBag:SetScript('OnEvent', SellAllInBag.OnEvent)
  51.  
  52. setmetatable(SellAllInBag, {
  53.     __index = getmetatable(SellAllInBag).__index;
  54.     __call = function(self, bagID)
  55.     --------------------------------------------------
  56.         if self.merchantAvailable then
  57.             if not self.currentBagID then
  58.                 self:SetCurrentBag(bagID)
  59.  
  60.                 local itemsToSell = self.itemsToSell
  61.                 for slot=1, GetContainerNumSlots(bagID) do
  62.                     local link = select(7, GetContainerItemInfo(bagID, slot))
  63.                     local sellPrice = (link and select(11, GetItemInfo(link))) or 0
  64.  
  65.                     if sellPrice > 0 then
  66.                         itemsToSell[#itemsToSell + 1] = slot
  67.                     end
  68.                 end
  69.  
  70.                 if #itemsToSell > 0 then
  71.                     self:AttemptSellItems()
  72.                 else
  73.                     self:SetCurrentBag(nil)
  74.                 end
  75.             else
  76.                 tinsert(self.pendingBags, bagID)
  77.             end
  78.         end
  79.     --------------------------------------------------
  80.     end;
  81. })

Here's a robust solution for you. Using C_Timer.After is bad for multiple reasons, one of which is asynchronous execution.
Unlike the others, this takes into account if you're actually interacting with an NPC and it is safe to use even if you have massive lag.
It's a bit over-engineered, but I think this is your best bet. You can also run this for multiple bags and it'll queue them up and finish them off in order.

The way it works is it attempts to sell as many items as possible at once, counting each item that goes through, but when the server throttles your request, it'll queue a new attempt to sell as many items as possible from the items you have remaining in your bag. It will look like chunks of items are disappearing out of your bags.

Usage:
Lua Code:
  1. SellAllInBag(0)
__________________

Last edited by MunkDev : 02-24-18 at 12:17 PM.
  Reply With Quote
02-25-18, 01:59 AM   #7
Eommus
A Fallenroot Satyr
Join Date: Apr 2017
Posts: 28
Thank you all for your inputs.

I tried the following, based on zork's sample code:

Code:
function SellAllInBag(bag)
	for slot = 1, GetContainerNumSlots(bag) do
		UseContainerItem(bag, slot)
	end
	C_Timer.After(0.1, SellAllInBag(bag))
end
It freezed the game. After I quit and logged back in, the items were sold. Probably I used C_Timer.After at the wrong place.

I tried Kanegasi's example, it sold only the first item in the bag and gave this error:

Code:
Message: Interface\AddOns\SellAll\SellAll.lua:3: Usage: C_Timer.After(seconds, func)
Time: 02/25/18 10:43:14
Count: 1
Stack: Interface\AddOns\SellAll\SellAll.lua:3: Usage: C_Timer.After(seconds, func)
[C]: ?
[C]: in function `After'
Interface\AddOns\SellAll\SellAll.lua:3: in function `SellAllInBag'
[string "*:OnClick"]:1: in function <[string "*:OnClick"]:1>

Locals:
I tried kurapica.igas sample code with coroutine, it sold only the first item in the bag and gave this error:

Code:
Message: attempt to yield across metamethod/C-call boundary
Time: 02/25/18 10:52:45
Count: 1
Stack: attempt to yield across metamethod/C-call boundary
[C]: ?
[C]: in function `yield'
Interface\AddOns\SellAll\SellAll.lua:4: in function <Interface\AddOns\SellAll\SellAll.lua:1>
Interface\AddOns\SellAll\SellAll.lua:10: in function `SellAllInBag'
[string "*:OnClick"]:1: in function <[string "*:OnClick"]:1>

Locals:
Ammako's solution seems to be working nicely, thank you again. I will continue my tests and give an update here shortly.

MunkDev, thank you for such a detailed solution. I always try to keep things as simple as possible, and in a way that I can understand and modify when needed. Can't tell if Ammako's solution is more or less efficient but it seems more preferrable to me due to its simplicity.

Just one thing: Will Ammako's timer(s) continue to tick in the background after all the items are sold in the bag? That wouldn't be ideal.

Last edited by Eommus : 02-25-18 at 02:09 AM.
  Reply With Quote
02-25-18, 06:38 AM   #8
MunkDev
A Scalebane Royal Guard
 
MunkDev's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2015
Posts: 402
It's a terrible solution for the reasons I mentioned. It can also break in the exact same way you wanted to work around if you're having a massive lag spike.
You need more than this to be able to handle server throttling, not causing errors due to attempting to use inventory items from code and handling when you step away from the NPC. It's not more complicated than it actually needs to be for the feature you want, especially if you're going to release it as an addon for others to use. My solution is more efficient because it doesn't use C_Timer and actually reacts to the event that tells you when your request is throttled.
  • UseContainerItem is a protected function, which doubles as a sell function. If you run a ticker function with UseContainerItem wrapped inside and you happen to be in combat, you had better keep talking to the NPC or you'll get action blocked messages.
  • If you step away from the merchant while the ticker is executing, you'll end up trying to use/equip every item left on the ticker.
  • Simplest code, but doesn't actually work properly.
__________________

Last edited by MunkDev : 02-25-18 at 06:51 AM.
  Reply With Quote
02-25-18, 09:17 AM   #9
Ammako
A Cobalt Mageweaver
 
Ammako's Avatar
AddOn Author - Click to view addons
Join Date: Jun 2016
Posts: 228
Your first attempt freezes the game because you recursively keep calling the function from within itself. Infinite loop.

Go with MunkDev's, they're way more experienced and know what they're talking about.
__________________
█████████████
█████████████
█████████████
█████████████
  Reply With Quote
02-25-18, 11:10 AM   #10
Kanegasi
A Cobalt Mageweaver
 
Kanegasi's Avatar
AddOn Author - Click to view addons
Join Date: Apr 2007
Posts: 232
I realized the problem with my code. I forgot C_Timer.After needs a direct function and cannot "call" one, as in it needs an anonymous function() end or the name of a function with no parenthesis. We're passing a function in as an argument to the After() function. Try this:

Lua Code:
  1. function SellAllInBag(bag)
  2.     for slot = 1, GetContainerNumSlots(bag) do
  3.         C_Timer.After(slot/10, function()
  4.             UseContainerItem(bag, slot)
  5.         end)
  6.     end
  7. end

I am sort of confused as to how the original even sold that first item.

If this works, as in it sells a bunch of items, but it doesn't sell everything, just adjust the slot/10. Even though the slot variable changes, the delay between each action is equal to the first delay, which is 1/10. If you wanted a delay of 0.5 sec, you would use slot/2.

Last edited by Kanegasi : 02-25-18 at 11:17 AM.
  Reply With Quote
02-25-18, 12:49 PM   #11
zork
A Pyroguard Emberseer
 
zork's Avatar
AddOn Author - Click to view addons
Join Date: Jul 2008
Posts: 1,686
Here is my approach: https://github.com/zorker/rothui/blo.../rSellPoor.lua
__________________
| Simple is beautiful.
| WoWI AddOns | GitHub | Zork (WoW) | TDMOG

"I wonder what the non-pathetic people are doing tonight?" - Rajesh Koothrappali (The Big Bang Theory)
  Reply With Quote

WoWInterface » Developer Discussions » Lua/XML Help » Adding a delay to the execution of a loop

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off