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.