game Lua scripting - using couroutine or polling?

Yellow_13 picture Yellow_13 · Jul 4, 2014 · Viewed 12.2k times · Source

I am starting to learn how to use Lua scripting for different game profile with logitech software.

First I tried to use onevent (I know it isn't very advanced) and created this attack combo script

function OnEvent(event, arg) 
    if event == "MOUSE_BUTTON_PRESSED" and arg == 1 then --set flag for mb1
        mb1_pressed = true
    elseif event == "MOUSE_BUTTON_RELEASED" and arg == 1 then --set flag for mb1=false
        mb1_pressed = false
    end
end

if mb1_pressed then --using flags to determine whether to start attack or not
    repeat
        presskey("A")
        Sleep(50)
        releasekey("A")
        Sleep(100)
        --if MB1 is release, it will also break script. if i only tap mb1, this will only execute the first line of attack without the rest below
        if not (**argument**, can be MB1/ismouse1) then break end
        presskey("S")
        Sleep(50)
        releasekey("")
        Sleep(120)
        presskey("A")
        Sleep(50)
        releasekey("A")
        Sleep(200)
        if not (**argument**, can be MB1/ismouse1) then break end --if MB1 is release, it will also break script. this point will prevent script from looping from start if mb1 release
    until not (**argument**, i use ismouse1) --end the loop of script
end

So I am trying to bind this to G6 button of my logiech mouse (using mouse_button_press == 6) Setting a flag with MB6 works, but ending a loop/breaking a loop cannot be triggered by MB6

After some research on SDK/Lua forum of logitech support, it seems that there is a problem with my script

  1. Flags cannot be used/detect as an argument while a script is performing a loop sequence
  2. IsMouseButtonPressed (reads windows keypress) can be used in place or arguments
  3. Windows only detects MB1-5, so binding to G6 is not possible (registers as 6th button)

I read that using couroutine.yield() or polling can be used for stopping repeat scripts in loop. But I cannot find a tutorial for beginners online.

Sorry for the noobish question!

Answer

hugomg picture hugomg · Jul 6, 2014

I don't know anything about Logitech mice so I will try to explain things using a simplified, pure Lua example. Lets model the autoattack script as a loop that prints "A" and "B" alternatively. The "A" corresponds to the first part of your loop (press and release A) and the "B" represents the second part (press and release S and A).

function autoattack()
    while true do
        print("A")
        print("B")
    end
end

autoattack()

So far we are OK but the loop will obviously run forever and we need to add a way to stop it. I think what you are trying to do is something along the lines of:

local autoattacking = false

function autoattack()
    autoattacking = true
    while true do
        print("A")
        if not autoattacking then break end
        print("B")
        if not autoattacking then break end
    end
end

function stop_autoattack()
    autoattacking = false
end

autoattack()    
stop_autoattack()

However, since autoattack is an infinite loop, stop_autoattack never runs and the autoattacking flag never gets updated. How can we fix this?

Polling

Instead of calling a function and setting a flag to stop the loop, what if we could call some code to see if the loop should be stopped or not?

function continue_autoattack()
    print("continue autoattacking? y/n")
    return (io.read("*l") == "y")
end 

function autoattack()
    while true do
        print("A")
        if not continue_autoattack() then break end
        print("B")
        if not continue_autoattack() then break end
    end
end

autoattack()

In your mouse this would probably mean using some sort of isKeyPressed function, if its available in the API. Its also important to note that the autoattack loop is still an infinite loop - its just that we changed it so it is in control of its stopping condition.

Coroutines

If we want to keep the code to stop the loop outside the loop we will need a way to run the autoattack loop one step at a time. Here is an example:

local state = 1
function autoattack_step()
    if state == 1 then
        print("A")
        state = 2
    elseif state == 2
        print("B")
        state = 1
    elseif state == 3
        print("STOPPED")
        --state remains as 3
    else
        error("bad state") -- defensive programming; I hate if/elseif without an else
    end
end

function stop_autoattack()
    state = 3
end

autoattack_step()
autoattack_step()
autoattack_step()
stop_autoattack()
autoattack_step()

Since we broke up the autoattack loop, we now have a chance to call stop_autoattack between calls to autoattack_step. To do this in your mouse script, I think stop_autoattack can go in "release button" handlers but I dont know where I would put the autoattack_step calls. Maybe the API includes something similar to setTimeout or setInterval in Javascript.

As for coroutines, where do they come in? Did you notice how we needed to do some substantial code refactoring to break the loop into single step chunks for autoattack_step? Coroutines are a Lua feature that lets you write code using loops while still being able to run them "one step at a time". When a coroutine reaches a coroutine.yield, it returns back to its caller. The thing is that when you call coroutine.resume again the coroutine will continue executing from where it stopped instead of going back to the start like a normal function would.

 local autoattacking = true

 autoattack = coroutine.create(function()
     while true do
         print("A")
         coroutine.yield()
         if not autoattacking then break end            
         print("B")
         coroutine.yield()
         if not autoattacking then break end
     end
 end)

 function stop_autoattack()
     autoattacking = false
 end

 coroutine.resume(autoattack)
 coroutine.resume(autoattack)
 coroutine.resume(autoattack)
 stop_autoattack()
 coroutine.resume(autoattack)
 coroutine.resume(autoattack)

Very often, coroutines let you keep code more readable, without turning inside out with lots of explicit "state" variables. We still need to have some "higher up" code calling coroutine.resume though, just like we needed to have some higher level code calling autoattack_step.