WoWInterface

WoWInterface (https://www.wowinterface.com/forums/index.php)
-   Lua/XML Help (https://www.wowinterface.com/forums/forumdisplay.php?f=16)
-   -   Adding a delay to the execution of a loop (https://www.wowinterface.com/forums/showthread.php?t=56063)

Eommus 02-23-18 03:36 AM

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.

zork 02-23-18 04:28 AM

I use C_Timer.After for that.
https://github.com/zorker/rothui/blo.../core.lua#L118

kurapica.igas 02-23-18 06:30 AM

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.

Kanegasi 02-23-18 08:25 PM

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.

Ammako 02-23-18 09:47 PM

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]).

MunkDev 02-24-18 11:45 AM

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)

Eommus 02-25-18 01:59 AM

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.

MunkDev 02-25-18 06:38 AM

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.

Ammako 02-25-18 09:17 AM

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. :p

Kanegasi 02-25-18 11:10 AM

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.

zork 02-25-18 12:49 PM

Here is my approach: https://github.com/zorker/rothui/blo.../rSellPoor.lua


All times are GMT -6. The time now is 06:43 PM.

vBulletin © 2024, Jelsoft Enterprises Ltd
© 2004 - 2022 MMOUI