summaryrefslogtreecommitdiff
path: root/indra/newview/scripts
diff options
context:
space:
mode:
authorMnikolenko Productengine <mnikolenko@productengine.com>2024-06-10 16:05:57 +0300
committerMnikolenko Productengine <mnikolenko@productengine.com>2024-06-10 16:05:57 +0300
commit7331d281e84da73907e1067b03ad4662991f4808 (patch)
treef911416d189a02775a21e3fc94b4bf8027e4ada9 /indra/newview/scripts
parente99c494418b4eec21ce3c17c5e642c253fae8084 (diff)
parentdbc785d4433080ca49b9cd899c756c9700a1a794 (diff)
Merge branch 'release/luau-scripting' into lua-ui-callbacks
Diffstat (limited to 'indra/newview/scripts')
-rw-r--r--indra/newview/scripts/lua/ErrorQueue.lua2
-rw-r--r--indra/newview/scripts/lua/Floater.lua32
-rw-r--r--indra/newview/scripts/lua/WaitQueue.lua2
-rw-r--r--indra/newview/scripts/lua/fiber.lua4
-rw-r--r--indra/newview/scripts/lua/leap.lua159
-rw-r--r--indra/newview/scripts/lua/printf.lua4
-rw-r--r--indra/newview/scripts/lua/test_timers.lua63
-rw-r--r--indra/newview/scripts/lua/timers.lua101
8 files changed, 310 insertions, 57 deletions
diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua
index 6ed1c10d5c..13e4e92941 100644
--- a/indra/newview/scripts/lua/ErrorQueue.lua
+++ b/indra/newview/scripts/lua/ErrorQueue.lua
@@ -3,8 +3,8 @@
-- raise that error.
local WaitQueue = require('WaitQueue')
--- local dbg = require('printf')
local function dbg(...) end
+-- local dbg = require('printf')
local ErrorQueue = WaitQueue:new()
diff --git a/indra/newview/scripts/lua/Floater.lua b/indra/newview/scripts/lua/Floater.lua
index 76efd47c43..75696533e4 100644
--- a/indra/newview/scripts/lua/Floater.lua
+++ b/indra/newview/scripts/lua/Floater.lua
@@ -46,10 +46,18 @@ function Floater:new(path, extra)
end
function Floater:show()
- local event = leap.request('LLFloaterReg', self._command)
+ -- leap.eventstream() returns the first response, and launches a
+ -- background fiber to call the passed callback with all subsequent
+ -- responses.
+ local event = leap.eventstream(
+ 'LLFloaterReg',
+ self._command,
+ -- handleEvents() returns false when done.
+ -- eventstream() expects a true return when done.
+ function(event) return not self:handleEvents(event) end)
self._pump = event.command_name
- -- we use the returned reqid to claim subsequent unsolicited events
- local reqid = event.reqid
+ -- we might need the returned reqid to cancel the eventstream() fiber
+ self.reqid = event.reqid
-- The response to 'showLuaFloater' *is* the 'post_build' event. Check if
-- subclass has a post_build() method. Honor the convention that if
@@ -57,22 +65,6 @@ function Floater:show()
if not self:handleEvents(event) then
return
end
-
- local waitfor = leap.WaitFor:new(-1, self.name)
- function waitfor:filter(pump, data)
- if data.reqid == reqid then
- return data
- end
- end
-
- fiber.launch(
- self.name,
- function ()
- event = waitfor:wait()
- while event and self:handleEvents(event) do
- event = waitfor:wait()
- end
- end)
end
function Floater:post(action)
@@ -125,7 +117,7 @@ function Floater:handleEvents(event_data)
-- We check for event() method before recognizing floater_close in case
-- the consumer needs to react specially to closing the floater. Now that
-- we've checked, recognize it ourselves. Returning false terminates the
- -- anonymous fiber function launched by show().
+ -- anonymous fiber function launched by leap.eventstream().
if event == _event('floater_close') then
LL.print_warning(self.name .. ' closed')
return false
diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua
index ad4fdecf43..6bcb9d62c2 100644
--- a/indra/newview/scripts/lua/WaitQueue.lua
+++ b/indra/newview/scripts/lua/WaitQueue.lua
@@ -5,8 +5,8 @@
local fiber = require('fiber')
local Queue = require('Queue')
--- local dbg = require('printf')
local function dbg(...) end
+-- local dbg = require('printf')
local WaitQueue = Queue:new()
diff --git a/indra/newview/scripts/lua/fiber.lua b/indra/newview/scripts/lua/fiber.lua
index 9057e6c890..cae27b936b 100644
--- a/indra/newview/scripts/lua/fiber.lua
+++ b/indra/newview/scripts/lua/fiber.lua
@@ -17,8 +17,8 @@
-- or with an error).
local printf = require 'printf'
--- local dbg = printf
local function dbg(...) end
+-- local dbg = printf
local coro = require 'coro'
local fiber = {}
@@ -303,6 +303,8 @@ function fiber.yield()
end
-- We're ready! Just return to caller. In this situation we don't care
-- whether there are other ready fibers.
+ dbg('fiber.yield() returning to %s (%sothers are ready)',
+ fiber.get_name(), ((not others) and "no " or ""))
end
-- Run fibers until all but main have terminated: return nil.
diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua
index cfb7377523..8caae24e94 100644
--- a/indra/newview/scripts/lua/leap.lua
+++ b/indra/newview/scripts/lua/leap.lua
@@ -40,6 +40,7 @@
local fiber = require('fiber')
local ErrorQueue = require('ErrorQueue')
+local inspect = require('inspect')
local function dbg(...) end
-- local dbg = require('printf')
@@ -74,7 +75,7 @@ local reply, command = LL.get_event_pumps()
-- pending is NOT a weak table because the caller of request() or generate()
-- never sees the WaitForReqid object. pending holds the only reference, so
-- it should NOT be garbage-collected.
-pending = {}
+local pending = {}
-- Our consumer will instantiate some number of WaitFor subclass objects.
-- As these are traversed in descending priority order, we must keep
-- them in a list.
@@ -82,7 +83,7 @@ pending = {}
-- to it. Once the consuming script drops the reference, allow Lua to
-- garbage-collect the WaitFor despite its entry in waitfors.
local weak_values = {__mode='v'}
-waitfors = setmetatable({}, weak_values)
+local waitfors = setmetatable({}, weak_values)
-- It has been suggested that we should use UUIDs as ["reqid"] values,
-- since UUIDs are guaranteed unique. However, as the "namespace" for
-- ["reqid"] values is our very own reply pump, we can get away with
@@ -131,7 +132,7 @@ local function requestSetup(pump, data)
local waitfor = leap.WaitForReqid:new(reqid)
pending[reqid] = waitfor
-- Pass reqid to send() to stamp it into (a copy of) the request data.
- dbg('requestSetup(%s, %s)', pump, data)
+ dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name)
leap.send(pump, data, reqid)
return reqid, waitfor
end
@@ -171,44 +172,127 @@ function leap.request(pump, data)
end
-- Send the specified request LLSD, expecting an arbitrary number of replies.
--- Each one is yielded on receipt. If you omit checklast, this is an infinite
--- generator; it's up to the caller to recognize when the last reply has been
--- received, and stop resuming for more.
---
--- If you pass checklast=<callable accepting(event)>, each response event is
--- passed to that callable (after the yield). When the callable returns
--- True, the generator terminates in the usual way.
+-- Each one is returned on request.
+--
+-- Usage:
+-- sequence = leap.generate(pump, data)
+-- repeat
+-- response = sequence.next()
+-- until last(response)
+-- (last() means whatever test the caller wants to perform on response)
+-- sequence.done()
--
-- See request() remarks about ["reqid"].
+--
+-- Note: this seems like a prime use case for Lua coroutines. But in a script
+-- using fibers.lua, a "wild" coroutine confuses the fiber scheduler. If
+-- generate() were itself a coroutine, it would call WaitForReqid:wait(),
+-- which would yield -- thereby resuming generate() WITHOUT waiting.
function leap.generate(pump, data, checklast)
-- Invent a new, unique reqid. Arrange to handle incoming events
-- bearing that reqid. Stamp the outbound request with that reqid, and
-- send it.
local reqid, waitfor = requestSetup(pump, data)
- local ok, response, resumed_with
- repeat
- ok, response = pcall(waitfor.wait, waitfor)
- if (not ok) or response.error then
- break
+ return {
+ next = function()
+ dbg('leap.generate(%s).next() about to wait on %s', reqid, tostring(waitfor))
+ local ok, response = pcall(waitfor.wait, waitfor)
+ dbg('leap.generate(%s).next() got %s: %s', reqid, ok, response)
+ if not ok then
+ error(response)
+ elseif response.error then
+ error(response.error)
+ else
+ return response
+ end
+ end,
+ done = function()
+ -- cleanup consists of removing our WaitForReqid from pending
+ pending[reqid] = nil
end
- -- can resume(false) to terminate generate() and clean up
- resumed_with = coroutine.yield(response)
- until (checklast and checklast(response)) or (resumed_with == false)
- -- If we break the above loop, whether or not due to error, clean up.
- pending[reqid] = nil
- if not ok then
- error(response)
- elseif response.error then
+ }
+end
+
+-- Send the specified request LLSD, expecting an immediate reply followed by
+-- an arbitrary number of subsequent replies with the same reqid. Block the
+-- calling coroutine until the first (immediate) reply, but launch a separate
+-- fiber on which to call the passed callback with later replies.
+--
+-- Once the callback returns true, the background fiber terminates.
+function leap.eventstream(pump, data, callback)
+ local reqid, waitfor = requestSetup(pump, data)
+ local response = waitfor:wait()
+ if response.error then
+ -- clean up our WaitForReqid
+ waitfor:close()
error(response.error)
end
+ -- No error, so far so good:
+ -- call the callback with the first response just in case
+ dbg('leap.eventstream(%s): first callback', reqid)
+ local ok, done = pcall(callback, response)
+ dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done)
+ if not ok then
+ -- clean up our WaitForReqid
+ waitfor:close()
+ error(done)
+ end
+ if done then
+ return response
+ end
+ -- callback didn't throw an error, and didn't say stop,
+ -- so set up to handle subsequent events
+ -- TODO: distinguish "daemon" fibers that can be terminated even if waiting
+ fiber.launch(
+ pump,
+ function ()
+ local ok, done
+ local nth = 1
+ repeat
+ event = waitfor:wait()
+ if not event then
+ -- wait() returns nil once the queue is closed (e.g. cancelreq())
+ ok, done = true, true
+ else
+ nth += 1
+ dbg('leap.eventstream(%s): callback %d', reqid, nth)
+ ok, done = pcall(callback, event)
+ dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done)
+ end
+ -- not ok means callback threw an error (caught as 'done')
+ -- done means callback succeeded but wants to stop
+ until (not ok) or done
+ -- once we break this loop, clean up our WaitForReqid
+ waitfor:close()
+ if not ok then
+ -- can't reflect the error back to our caller
+ LL.print_warning(fiber.get_name() .. ': ' .. done)
+ end
+ end)
+ return response
+end
+
+-- we might want to clean up after leap.eventstream() even if the callback has
+-- not yet returned true
+function leap.cancelreq(reqid)
+ dbg('cancelreq(%s)', reqid)
+ local waitfor = pending[reqid]
+ if waitfor ~= nil then
+ -- close() removes the pending entry and also closes the queue,
+ -- breaking the background fiber's wait loop.
+ dbg('cancelreq(%s) canceling %s', reqid, waitfor.name)
+ waitfor:close()
+ end
end
local function cleanup(message)
- -- we're done: clean up all pending coroutines
- for i, waitfor in pairs(pending) do
+ -- We're done: clean up all pending coroutines.
+ -- Iterate over copies of the pending and waitfors tables, since the
+ -- close() operation modifies the real tables.
+ for i, waitfor in pairs(table.clone(pending)) do
waitfor:close()
end
- for i, waitfor in pairs(waitfors) do
+ for i, waitfor in pairs(table.clone(waitfors)) do
waitfor:close()
end
end
@@ -223,7 +307,8 @@ local function unsolicited(pump, data)
return
end
end
- LL.print_debug(string.format('unsolicited(%s, %s) discarding unclaimed event', pump, data))
+ LL.print_debug(string.format('unsolicited(%s, %s) discarding unclaimed event',
+ pump, inspect(data)))
end
-- Route incoming (pump, data) event to the appropriate waiting coroutine.
@@ -231,14 +316,17 @@ local function dispatch(pump, data)
local reqid = data['reqid']
-- if the response has no 'reqid', it's not from request() or generate()
if reqid == nil then
+-- dbg('dispatch() found no reqid; calling unsolicited(%s, %s)', pump, data)
return unsolicited(pump, data)
end
-- have reqid; do we have a WaitForReqid?
local waitfor = pending[reqid]
if waitfor == nil then
+-- dbg('dispatch() found no WaitForReqid(%s); calling unsolicited(%s, %s)', reqid, pump, data)
return unsolicited(pump, data)
end
-- found the right WaitForReqid object, let it handle the event
+-- dbg('dispatch() calling %s.handle(%s, %s)', waitfor.name, pump, data)
waitfor:handle(pump, data)
end
@@ -284,11 +372,9 @@ end
-- called by WaitFor.disable()
local function unregisterWaitFor(waitfor)
- for i, w in pairs(waitfors) do
- if w == waitfor then
- waitfors[i] = nil
- break
- end
+ local i = table.find(waitfors, waitfor)
+ if i ~= nil then
+ waitfors[i] = nil
end
end
@@ -417,6 +503,7 @@ end
-- called by cleanup() at end
function leap.WaitFor:close()
+ self:disable()
self._queue:close()
end
@@ -437,6 +524,8 @@ function leap.WaitForReqid:new(reqid)
setmetatable(obj, self)
self.__index = self
+ obj.reqid = reqid
+
return obj
end
@@ -447,4 +536,10 @@ function leap.WaitForReqid:filter(pump, data)
return data
end
+function leap.WaitForReqid:close()
+ -- remove this entry from pending table
+ pending[self.reqid] = nil
+ self._queue:close()
+end
+
return leap
diff --git a/indra/newview/scripts/lua/printf.lua b/indra/newview/scripts/lua/printf.lua
index 584cd4f391..e84b2024df 100644
--- a/indra/newview/scripts/lua/printf.lua
+++ b/indra/newview/scripts/lua/printf.lua
@@ -2,7 +2,7 @@
local inspect = require 'inspect'
-local function printf(...)
+local function printf(format, ...)
-- string.format() only handles numbers and strings.
-- Convert anything else to string using the inspect module.
local args = {}
@@ -13,7 +13,7 @@ local function printf(...)
table.insert(args, inspect(arg))
end
end
- print(string.format(table.unpack(args)))
+ print(string.format(format, table.unpack(args)))
end
return printf
diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua
new file mode 100644
index 0000000000..ed0de070f7
--- /dev/null
+++ b/indra/newview/scripts/lua/test_timers.lua
@@ -0,0 +1,63 @@
+local timers = require 'timers'
+
+-- This t0 is constructed for 10 seconds, but its purpose is to exercise the
+-- query and cancel methods. It would print "t0 fired at..." if it fired, but
+-- it doesn't, so you don't see that message. Instead you see that isRunning()
+-- is true, that timeUntilCall() is (true, close to 10), that cancel() returns
+-- true. After that, isRunning() is false, timeUntilCall() returns (false, 0),
+-- and a second cancel() returns false.
+print('t0:new(10)')
+start = os.clock()
+t0 = timers.Timer:new(10, function() print('t0 fired at', os.clock() - start) end)
+print('t0:isRunning(): ', t0:isRunning())
+print('t0:timeUntilCall(): ', t0:timeUntilCall())
+print('t0:cancel(): ', t0:cancel())
+print('t0:isRunning(): ', t0:isRunning())
+print('t0:timeUntilCall(): ', t0:timeUntilCall())
+print('t0:cancel(): ', t0:cancel())
+
+-- t1 is supposed to fire after 5 seconds, but it doesn't wait, so you see the
+-- t2 messages immediately after.
+print('t1:new(5)')
+start = os.clock()
+t1 = timers.Timer:new(5, function() print('t1 fired at', os.clock() - start) end)
+
+-- t2 illustrates that instead of passing a callback to new(), you can
+-- override the timer instance's tick() method. But t2 doesn't wait either, so
+-- you see the Timer(5) message immediately.
+print('t2:new(2)')
+start = os.clock()
+t2 = timers.Timer:new(2)
+function t2:tick()
+ print('t2 fired at', os.clock() - start)
+end
+
+-- This anonymous timer blocks the calling fiber for 5 seconds. Other fibers
+-- are free to run during that time, so you see the t2 callback message and
+-- then the t1 callback message before the Timer(5) completion message.
+print('Timer(5) waiting')
+start = os.clock()
+timers.Timer:new(5, 'wait')
+print(string.format('Timer(5) waited %f seconds', os.clock() - start))
+
+-- This test demonstrates a repeating timer. It also shows that you can (but
+-- need not) use a coroutine as the timer's callback function: unlike Python,
+-- Lua doesn't disinguish between yield() and return. A coroutine wrapped with
+-- coroutine.wrap() looks to Lua just like any other function that you can
+-- call repeatedly and get a result each time. We use that to count the
+-- callback calls and stop after a certain number. Of course that could also
+-- be arranged in a plain function by incrementing a script-scope counter, but
+-- it's worth knowing that a coroutine timer callback can be used to manage
+-- more complex control flows.
+start = os.clock()
+timers.Timer:new(
+ 2,
+ coroutine.wrap(function()
+ for i = 1,5 do
+ print('repeat(2) timer fired at ', os.clock() - start)
+ coroutine.yield(nil) -- keep running
+ end
+ print('repeat(2) timer fired last at ', os.clock() - start)
+ return true -- stop
+ end),
+ true) -- iterate
diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/timers.lua
new file mode 100644
index 0000000000..e0d27a680d
--- /dev/null
+++ b/indra/newview/scripts/lua/timers.lua
@@ -0,0 +1,101 @@
+-- Access to the viewer's time-delay facilities
+
+local leap = require 'leap'
+
+local timers = {}
+
+local function dbg(...) end
+-- local dbg = require 'printf'
+
+timers.Timer = {}
+
+-- delay: time in seconds until callback
+-- callback: 'wait', or function to call when timer fires (self:tick if nil)
+-- iterate: if non-nil, call callback repeatedly until it returns non-nil
+-- (ignored if 'wait')
+function timers.Timer:new(delay, callback, iterate)
+ local obj = setmetatable({}, self)
+ self.__index = self
+
+ if callback == 'wait' then
+ dbg('scheduleAfter(%d):', delay)
+ sequence = leap.generate('Timers', {op='scheduleAfter', after=delay})
+ -- ignore the immediate return
+ dbg('scheduleAfter(%d) -> %s', delay,
+ sequence.next())
+ -- this call is where we wait for real
+ dbg('next():')
+ dbg('next() -> %s',
+ sequence.next())
+ sequence.done()
+ return
+ end
+
+ callback = callback or function() obj:tick() end
+
+ local first = true
+ if iterate then
+ obj.id = leap.eventstream(
+ 'Timers',
+ {op='scheduleEvery', every=delay},
+ function (event)
+ local reqid = event.reqid
+ if first then
+ first = false
+ dbg('timer(%s) first callback', reqid)
+ -- discard the first (immediate) response: don't call callback
+ return nil
+ else
+ dbg('timer(%s) nth callback', reqid)
+ return callback(event)
+ end
+ end
+ ).reqid
+ else
+ obj.id = leap.eventstream(
+ 'Timers',
+ {op='scheduleAfter', after=delay},
+ function (event)
+ -- Arrange to return nil the first time, true the second. This
+ -- callback is called immediately with the response to
+ -- 'scheduleAfter', and if we immediately returned true, we'd
+ -- be done, and the subsequent timer event would be discarded.
+ if first then
+ first = false
+ -- Caller doesn't expect an immediate callback.
+ return nil
+ else
+ callback(event)
+ -- Since caller doesn't want to iterate, the value
+ -- returned by the callback is irrelevant: just stop after
+ -- this one and only call.
+ return true
+ end
+ end
+ ).reqid
+ end
+
+ return obj
+end
+
+function timers.Timer:tick()
+ error('Pass a callback to Timer:new(), or override Timer:tick()')
+end
+
+function timers.Timer:cancel()
+ local ok = leap.request('Timers', {op='cancel', id=self.id}).ok
+ leap.cancelreq(self.id)
+ return ok
+end
+
+function timers.Timer:isRunning()
+ return leap.request('Timers', {op='isRunning', id=self.id}).running
+end
+
+-- returns (true, seconds left) for a live timer, else (false, 0)
+function timers.Timer:timeUntilCall()
+ local result = leap.request('Timers', {op='timeUntilCall', id=self.id})
+ return result.ok, result.remaining
+end
+
+return timers