LuaCore 1.7.0.0 - The Snooze Edition

Eorzea Time
 
 
 
Language: JP EN FR DE
Version 3.1
New Items
users online
Forum » Windower » News » LuaCore 1.7.0.0 - the snooze edition
LuaCore 1.7.0.0 - the snooze edition
 Leviathan.Arcon
VIP
Offline
Server: Leviathan
Game: FFXI
user: Zaphor
Posts: 660
By Leviathan.Arcon 2014-05-27 13:41:18
Link | Quote | Reply
 
Disclaimer

This post is mostly intended for addon developers, it will get a bit technical and may not be understood by many end-users. However, if you're deep into advanced GS scripting, you may also want to read this, as it will allow you to handle a few things better.

Also, this post mentions coroutines, but you will not need to understand the concept to make good use of the new features.


Sleeping

For a while now LuaCore has lacked a decent support for a sleep mechanism. There was a windower.sleep function, but it had a few significant caveats and restrictions, and if not used very carefully it would freeze the entire game, possibly causing a server disconnect.

Now we abandoned that function entirely and made a new one: coroutine.sleep

This one works almost like expected, but it also has one thing that may be considered unusual behavior which I'll explain later. For now I'll get to the three functions that will be relevant for this LuaCore release.

coroutine.yield

This function is similar to a return statement. Any arguments passed to it will be considered return values of the function. Take this example:
Code
windower.register_event('outgoing text', function(text)
    if text:match('LS') then
        return text:gsub('LS', 'losers')
    end
end


If you load this code in an addon and type "Hey LS!" it will translate it to "Hey losers!" before sending the message. Now assume that in addition to changing "LS" to "losers" you want to do some analysis of the line you sent. And let's assume that the analysis is really complex and takes a lot of time (say loading and storing to an external database, or send commands over a network):
Code
windower.register_event('outgoing text', function(text)
    -- Some expensive computations here

    if text:match('LS') then
        return text:gsub('LS', 'losers')
    end
end


The problem with this code now is that every time you type something in the chatlog or use a FFXI macro or anything of that kind, the expensive computations will have to be performed. If they take a second or two to perform, you would notice a huge lag between sending commands and the commands actually having an effect. This is obviously not wanted. And that's a problem that yielding can solve. For that we need to change the return statement to a yield statement and make it appear before the expensive computations:
Code
windower.register_event('outgoing text', function(text)
    if text:match('LS') then
        coroutine.yield(text:gsub('LS', 'losers'))
    end

    -- Some expensive computations here
end


And this is the part that's impossible to do without yielding. If we left the return part in, this code would not do the computations at all if we return before them. However, if we left the return after the computations, we would experience significant lag. coroutine.yield has the interesting property that it returns from the function, but immediately returns to the function when it has the time to finish the remaining computations. So it's a return that can continue where it left from.

One thing to note about this is that you cannot return any more values after the yield. Once it's yielded the event is done processing as far as LuaCore is concerned, and it will pass the event itself further down the chain of evaluation (either to the next addon, or, if no other addon has the event registered, back to FFXI).

One last simple example that shows how yielding functions:
Code
windower.register_event('incoming text', function(text)
    if text:match('test') then
        windower.add_to_chat(207, 'before')
        coroutine.yield(text:gsub('test', 'success'))
        windower.add_to_chat(207, 'after')
    end
end


Now typing "/echo test" will result in "before" being printed to the chatlog, followed by "success" being echo'd and then "after" being printed. This shows how the coroutine returned to continue the evaluation where the yield left off.

coroutine.sleep

This function will sleep the current function's evaluation for the given amount of time (in seconds). So far, so good, but one thing needs to be noted: it's implemented as an implicit yield, meaning that as soon as you call it, the function will return. It will then be scheduled to continue running after the time provided has passed. So functionally speaking, it's the same as calling coroutine.yield() before every time you call coroutine.sleep().

A demonstration of how sleeping works using the Eval addon (which interprets Lua commands from FFXI):
Code
//eval coroutine.sleep(1) print(2); eval print(1)


This will print "1" immediately, followed by "2" one second later.

coroutine.schedule

This function takes two arguments, a function and an amount of time to wait before the function should be executed. The current code will keep running and the specified function will be executed in its own coroutine, meaning yielding and sleeping in that function will have no effect on the current function.

A small example:
Code
coroutine.schedule(function()
    print(1)
    coroutine.sleep(2)
    print(3)
end, 1000)

print(0)
coroutine.sleep(2)
print(2)


This will schedule a function to run 1 second from now. Then it prints "0" immediately and sleeps for 2 seconds. Meanwhile, the scheduled function begins to run which prints "1" and then immediately goes to sleep itself for 2 seconds.

In that time, the original code will wake up, since its 2 seconds have passed and print "2". Finally the scheduled function will wake up again and print the final "3". So this will print the numbers 0 through 3 in one-second intervals.

Meaning for addons

Looping

Several addons currently rely on timed loops to run. The way this was accomplished so far was to abuse Windower's command handler and invoke its own functions through the command interface. An example from the AutoJoin addon, which tries to join but checks if something is in the pool, and if so, delays the join by one second. After one second it will check the pool again, and keep doing so until the pool clears (slightly adjusted the code for readability:
Code
join = function()
    if not table.empty(windower.ffxi.get_items().treasure) then
        windower.send_command('@wait 1; lua invoke autojoin join')
    else
        packets.inject(join_packet)
    end
end()


With coroutines and sleeping, this can be changed to the following:
Code
join = function()
    while not table.empty(windower.ffxi.get_items().treasure) do
        coroutine.sleep(1)
    end

    packets.inject(join_packet)
end()


It doesn't save that much space in this case, but the logic is fully contained in the function now and never leaves it until it's done. It's also easier to adjust the function for other variables. Up until now you needed to either make the variables global or wrap the function in a closure. Now, however, they can be local since they don't have to be maintained through repeated function calls. For example, we can easily make it quit trying after the max /join time (90 seconds):
Code
join = function()
    local time = 0
    while not table.empty(windower.ffxi.get_items().treasure) do
        coroutine.sleep(1)
        time = time + 1

        if time > 90 then
            return
        end
    end

    packets.inject(join_packet)
end()


Timed events

Some addons rely on periodic events to perform computations. Scoreboard, for example, tries to update as much as possible without introducing significant delay. Since its computation is more expensive than most addons', it doesn't use the prerender event, which triggers up to 30 times per second (on every render frame). Currently it relies on the several events, some of which it doesn't even use, just to force updates regularly. This can now be solved by using a timer to execute a function periodically:
Code
local function update_dps_clock()
    while true do
        local player = windower.ffxi.get_player()
        if player and player.in_combat then
            dps_clock:advance()
        else
            dps_clock:pause()
        end

        coroutine.sleep(0.2)
    end
end


This would now update every 200 milliseconds, so five times a second. And it can now be adjust to whatever people prefer. If someone plays on a slower machine they may wanna slow it down to once a second, and Scoreboard could just read the value from the user's settings file:
Code
local function update_dps_clock()
    while true do
        -- Stuff

        coroutine.sleep(settings.UpdateFrequency)
    end
end


Using the functions library, the code can be shortened to this:
Code
function()
    -- Stuff
end:loop(settings.UpdateFrequency)


Delayed evaluation

Some addons wanna execute certain functions when a player logs in. However, immediately upon login not everything may be available. For example, it takes up to 20 seconds for all items to download from the server, and if an addon wants to use those on login, it has to wait for up to 20 seconds to initialize. This can now be done with the new scheduler. Usually you'd write this to execute a function on login:
Code
windower.register_event('login', function(name)
    -- Function body here
end)


You can now wrap it in a scheduled coroutine to delay it. However, to carry the argument you need to add some boilerplate code:
Code
windower.register_event('login', function(...)
    coroutine.schedule(function(...)
        local name = ...
        return function()
            -- Function body here
        end
    end(...), 20)
end)


This is a bit long, but using the functions library it can be shortened significantly again:
Code
windower.register_event('login', function(name)
    -- Function body here
end:delay(20))


This will now wait 20 seconds to run the function.
[+]
 Cerberus.Conagh
Offline
Server: Cerberus
Game: FFXI
user: onagh
Posts: 3189
By Cerberus.Conagh 2014-05-27 14:16:26
Link | Quote | Reply
 
The general gist seems to be "you get less lag and better performance with a better UI".

Seems like a win
 Leviathan.Malthar
Offline
Server: Leviathan
Game: FFXI
user: Malthar
Posts: 43
By Leviathan.Malthar 2014-05-27 15:39:25
Link | Quote | Reply
 
What does the @ mean in @wait? I have been searching for a description for years but cannot fnd any documentation for it.
 Leviathan.Arcon
VIP
Offline
Server: Leviathan
Game: FFXI
user: Zaphor
Posts: 660
By Leviathan.Arcon 2014-05-27 16:15:44
Link | Quote | Reply
 
It executes the command asynchronously, meaning the command handler returns before the command is fully processed. Prior to Windower 4 it was rarely used, but precisely due to the lack of a way to sleep before now, we ran into crashes due to overflowing the stack, because all commands would queue and not return before all (possibly infinitely many) sub-commands finished. This was obviously a problem, one that @ meant to address. With a proper sleeping mechanism added now you should very rarely need to use @ anymore (I can't think of an example where I'd use it right now).
necroskull Necro Bump Detected! [2342 days between previous and next post]
Offline
Posts: 34
By shastax 2020-10-24 15:38:33
Link | Quote | Reply
 
Is there a way to clear the scheduler of outstanding commands? For example if I execute a command with a 120s wait and then change jobs and want to cancel that command, is that possible?