diff options
Diffstat (limited to 'indra/newview')
37 files changed, 523 insertions, 110 deletions
diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp index 60571d6247..f715327ec8 100644 --- a/indra/newview/llfloaterluadebug.cpp +++ b/indra/newview/llfloaterluadebug.cpp @@ -41,6 +41,7 @@ #include "llsdutil.h" #include "lua_function.h" #include "stringize.h" +#include "tempset.h" LLFloaterLUADebug::LLFloaterLUADebug(const LLSD &key) @@ -77,6 +78,17 @@ LLFloaterLUADebug::~LLFloaterLUADebug() void LLFloaterLUADebug::onExecuteClicked() { + // Empirically, running Lua code that indirectly invokes the + // "LLNotifications" listener can result (via mysterious labyrinthine + // viewer UI byways) in a recursive call to this handler. We've seen Bad + // Things happen to the viewer with a second call to runScriptLine() with + // the same cmd on the same LuaState. + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to onExecuteClicked()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); mResultOutput->setValue(""); std::string cmd = mLineInput->getText(); @@ -94,6 +106,12 @@ void LLFloaterLUADebug::onBtnBrowse() void LLFloaterLUADebug::onBtnRun() { + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to onBtnRun()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); std::vector<std::string> filenames; std::string filepath = mScriptPath->getText(); if (!filepath.empty()) @@ -105,6 +123,12 @@ void LLFloaterLUADebug::onBtnRun() void LLFloaterLUADebug::runSelectedScript(const std::vector<std::string> &filenames) { + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to runSelectedScript()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); mResultOutput->setValue(""); std::string filepath = filenames[0]; @@ -129,13 +153,19 @@ void LLFloaterLUADebug::completion(int count, const LLSD& result) mResultOutput->endOfDoc(); return; } + if (count == 0) + { + // no results + mResultOutput->pasteTextWithLinebreaks(stringize("ok ", ++mAck)); + return; + } if (count == 1) { // single result mResultOutput->pasteTextWithLinebreaks(stringize(result)); return; } - // 0 or multiple results + // multiple results const char* sep = ""; for (const auto& item : llsd::inArray(result)) { diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h index 7418174570..ae30b7cf25 100644 --- a/indra/newview/llfloaterluadebug.h +++ b/indra/newview/llfloaterluadebug.h @@ -66,6 +66,8 @@ private: LLLineEditor* mLineInput; LLLineEditor* mScriptPath; LuaState mState; + U32 mAck{ 0 }; + bool mExecuting{ false }; }; #endif // LL_LLFLOATERLUADEBUG_H diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 97779a12ad..3ed72c34f3 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -139,10 +139,10 @@ lua_function(get_event_pumps, "post_on(commandpump, ...) to engage LLEventAPI operations (see helpleap()).") { luaL_checkstack(L, 2, nullptr); - auto listener{ LuaState::obtainListener(L) }; + auto& listener{ LuaState::obtainListener(L) }; // return the reply pump name and the command pump name on caller's lua_State - lua_pushstdstring(L, listener->getReplyName()); - lua_pushstdstring(L, listener->getCommandName()); + lua_pushstdstring(L, listener.getReplyName()); + lua_pushstdstring(L, listener.getCommandName()); return 2; } @@ -153,8 +153,8 @@ lua_function(get_event_next, "event becomes available.") { luaL_checkstack(L, 2, nullptr); - auto listener{ LuaState::obtainListener(L) }; - const auto& [pump, data]{ listener->getNext() }; + auto& listener{ LuaState::obtainListener(L) }; + const auto& [pump, data]{ listener.getNext() }; lua_pushstdstring(L, pump); lua_pushllsd(L, data); lluau::set_interrupts_counter(L, 0); @@ -254,7 +254,7 @@ void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_r if (shortchunk.length() > shortlen) shortchunk = stringize(shortchunk.substr(0, shortlen), "..."); - std::string desc{ stringize("lua: ", shortchunk) }; + std::string desc{ "lua: " + shortchunk }; LLCoros::instance().launch(desc, [&L, desc, chunk, cb]() { auto [count, result] = L.expr(desc, chunk); @@ -386,10 +386,10 @@ void LLRequireResolver::findModule() std::vector<fsyspath> lib_paths { - gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"), + gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua", "require"), #ifdef LL_TEST // Build-time tests don't have the app bundle - use source tree. - fsyspath(__FILE__).parent_path() / "scripts" / "lua", + fsyspath(__FILE__).parent_path() / "scripts" / "lua" / "require", #endif }; @@ -454,45 +454,70 @@ bool LLRequireResolver::findModuleImpl(const std::string& absolutePath) void LLRequireResolver::runModule(const std::string& desc, const std::string& code) { // Here we just loaded a new module 'code', need to run it and get its result. - // Module needs to run in a new thread, isolated from the rest. - // Note: we create ML on main thread so that it doesn't inherit environment of L. - lua_State *GL = lua_mainthread(L); -// lua_State *ML = lua_newthread(GL); - // Try loading modules on Lua's main thread instead. - lua_State *ML = GL; - // lua_newthread() pushed the new thread object on GL's stack. Move to L's. -// lua_xmove(GL, L, 1); - - // new thread needs to have the globals sandboxed -// luaL_sandboxthread(ML); + lua_State *ML = lua_mainthread(L); { // If loadstring() returns (! LUA_OK) then there's an error message on // the stack. If it returns LUA_OK then the newly-loaded module code // is on the stack. - if (lluau::loadstring(ML, desc, code) == LUA_OK) + LL_DEBUGS("Lua") << "Loading module " << desc << LL_ENDL; + if (lluau::loadstring(ML, desc, code) != LUA_OK) { - // luau uses Lua 5.3's version of lua_resume(): - // run the coroutine on ML, "from" L, passing no arguments. -// int status = lua_resume(ML, L, 0); - // we expect one return value - int status = lua_pcall(ML, 0, 1, 0); + // error message on stack top + LL_DEBUGS("Lua") << "Error loading module " << desc << ": " + << lua_tostring(ML, -1) << LL_ENDL; + lua_pushliteral(ML, "loadstring: "); + // stack contains error, "loadstring: " + // swap: insert stack top at position -2 + lua_insert(ML, -2); + // stack contains "loadstring: ", error + lua_concat(ML, 2); + // stack contains "loadstring: " + error + } + else // module code on stack top + { + // push debug module + lua_getglobal(ML, "debug"); + // push debug.traceback + lua_getfield(ML, -1, "traceback"); + // stack contains module code, debug, debug.traceback + // ditch debug + lua_replace(ML, -2); + // stack contains module code, debug.traceback + // swap: insert stack top at position -2 + lua_insert(ML, -2); + // stack contains debug.traceback, module code + LL_DEBUGS("Lua") << "Loaded module " << desc << ", running" << LL_ENDL; + // no arguments, one return value + // pass debug.traceback as the error function + int status = lua_pcall(ML, 0, 1, -2); + // lua_pcall() has popped the module code and replaced it with its + // return value. Regardless of status or the type of the stack + // top, get rid of debug.traceback on the stack. + lua_remove(ML, -2); if (status == LUA_OK) { - if (lua_gettop(ML) == 0) - lua_pushfstring(ML, "module %s must return a value", desc.data()); - else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1)) + auto top{ lua_gettop(ML) }; + std::string type{ (top == 0)? "nothing" + : lua_typename(ML, lua_type(ML, -1)) }; + LL_DEBUGS("Lua") << "Module " << desc << " returned " << type << LL_ENDL; + if ((top == 0) || ! (lua_istable(ML, -1) || lua_isfunction(ML, -1))) + { lua_pushfstring(ML, "module %s must return a table or function, not %s", - desc.data(), lua_typename(ML, lua_type(ML, -1))); + desc.data(), type.data()); + } } else if (status == LUA_YIELD) { + LL_DEBUGS("Lua") << "Module " << desc << " yielded" << LL_ENDL; lua_pushfstring(ML, "module %s can not yield", desc.data()); } - else if (!lua_isstring(ML, -1)) + else { - lua_pushfstring(ML, "unknown error while running module %s", desc.data()); + llassert(lua_isstring(ML, -1)); + LL_DEBUGS("Lua") << "Module " << desc << " error: " + << lua_tostring(ML, -1) << LL_ENDL; } } } @@ -502,8 +527,4 @@ void LLRequireResolver::runModule(const std::string& desc, const std::string& co { lua_xmove(ML, L, 1); } - // remove ML from L's stack -// lua_remove(L, -2); -// // DON'T call lua_close(ML)! Since ML is only a thread of L, corrupts L too! -// lua_close(ML); } diff --git a/indra/newview/llwindowlistener.cpp b/indra/newview/llwindowlistener.cpp index 87d18cfc00..efe395d9ca 100644 --- a/indra/newview/llwindowlistener.cpp +++ b/indra/newview/llwindowlistener.cpp @@ -55,7 +55,7 @@ LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter& "Given [\"keysym\"], [\"keycode\"] or [\"char\"], inject the specified "; std::string keyExplain = "(integer keycode values, or keysym string from any addKeyName() call in\n" - "http://bitbucket.org/lindenlab/viewer-release/src/tip/indra/llwindow/llkeyboard.cpp )\n"; + "https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124)\n"; std::string mask = "Specify optional [\"mask\"] as an array containing any of \"CTL\", \"ALT\",\n" "\"SHIFT\" or \"MAC_CONTROL\"; the corresponding modifier bits will be combined\n" diff --git a/indra/newview/scripts/lua/LLNotification.lua b/indra/newview/scripts/lua/LLNotification.lua deleted file mode 100644 index f47730d1cc..0000000000 --- a/indra/newview/scripts/lua/LLNotification.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Engage the LLNotificationsListener LLEventAPI - -leap = require 'leap' - -local LLNotification = {} - -function LLNotification.add(name, substitutions) - leap.send('LLNotifications', {op='requestAdd', name=name, substitutions=substitutions}) -end - -function LLNotification.requestAdd(name, substitutions) - return leap.request('LLNotifications', {op='requestAdd', name=name, substitutions=substitutions})['response'] -end - -return LLNotification diff --git a/indra/newview/scripts/lua/UI.lua b/indra/newview/scripts/lua/UI.lua deleted file mode 100644 index 24f822bbd9..0000000000 --- a/indra/newview/scripts/lua/UI.lua +++ /dev/null @@ -1,16 +0,0 @@ --- Engage the UI LLEventAPI - -leap = require 'leap' - -local UI = {} - -function UI.call(func, parameter) - -- 'call' is fire-and-forget - leap.request('UI', {op='call', ['function']=func, parameter=parameter}) -end - -function UI.getValue(path) - return leap.request('UI', {op='getValue', path=path})['value'] -end - -return UI diff --git a/indra/newview/scripts/lua/login.lua b/indra/newview/scripts/lua/login.lua new file mode 100644 index 0000000000..0d8591cace --- /dev/null +++ b/indra/newview/scripts/lua/login.lua @@ -0,0 +1,19 @@ +local UI = require 'UI' +local leap = require 'leap' + +local function login(username, password) + if username and password then + local userpath = '//username_combo/Combo Text Entry' + local passpath = '//password_edit' + -- first clear anything presently in those text fields + for _, path in pairs({userpath, passpath}) do + UI.click(path) + UI.keypress{keysym='Backsp', path=path} + end + UI.type{path=userpath, text=username} + UI.type{path=passpath, text=password} + end + leap.send('LLPanelLogin', {op='onClickConnect'}) +end + +return login diff --git a/indra/newview/scripts/lua/mapargs.lua b/indra/newview/scripts/lua/mapargs.lua new file mode 100644 index 0000000000..45f5a9c556 --- /dev/null +++ b/indra/newview/scripts/lua/mapargs.lua @@ -0,0 +1,73 @@ +-- Allow a calling function to be passed a mix of positional arguments with +-- keyword arguments. Reference them as fields of a table. +-- Don't use this for a function that can accept a single table argument. +-- mapargs() assumes that a single table argument means its caller was called +-- with f{table constructor} syntax, and maps that table to the specified names. +-- Usage: +-- function f(...) +-- local a = mapargs({'a1', 'a2', 'a3'}, ...) +-- ... a.a1 ... etc. +-- end +-- f(10, 20, 30) -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{10, 20, 30} -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{a3=300, a1=100} -- a.a1 == 100, a.a2 == nil, a.a3 == 300 +-- f{1, a3=3} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +-- f{a3=3, 1} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +local function mapargs(names, ...) + local args = table.pack(...) + local posargs = {} + local keyargs = {} + -- For a mixed table, no Lua operation will reliably tell you how many + -- array items it contains, if there are any holes. Track that by hand. + -- We must be able to handle f(1, nil, 3) calls. + local maxpos = 0 + + -- For convenience, allow passing 'names' as a string 'n0,n1,...' + if type(names) == 'string' then + names = string.split(names, ',') + end + + if not (args.n == 1 and type(args[1]) == 'table') then + -- If caller passes more than one argument, or if the first argument + -- is not a table, then it's classic positional function-call syntax: + -- f(first, second, etc.). In that case we need not bother teasing + -- apart positional from keyword arguments. + posargs = args + maxpos = args.n + else + -- Single table argument implies f{mixed} syntax. + -- Tease apart positional arguments from keyword arguments. + for k, v in pairs(args[1]) do + if type(k) == 'number' then + posargs[k] = v + maxpos = math.max(maxpos, k) + else + if table.find(names, k) == nil then + error('unknown keyword argument ' .. tostring(k)) + end + keyargs[k] = v + end + end + end + + -- keyargs already has keyword arguments in place, just fill in positionals + args = keyargs + -- Don't exceed the number of parameter names. Loop explicitly over every + -- index value instead of using ipairs() so we can support holes (nils) in + -- posargs. + for i = 1, math.min(#names, maxpos) do + if posargs[i] ~= nil then + -- As in Python, make it illegal to pass an argument both positionally + -- and by keyword. This implementation permits func(17, first=nil), a + -- corner case about which I don't particularly care. + if args[names[i]] ~= nil then + error(string.format('parameter %s passed both positionally and by keyword', + tostring(names[i]))) + end + args[names[i]] = posargs[i] + end + end + return args +end + +return mapargs diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua index 009446d0c3..9526f58b04 100644 --- a/indra/newview/scripts/lua/qtest.lua +++ b/indra/newview/scripts/lua/qtest.lua @@ -21,8 +21,8 @@ function resume(co, ...) end -- ------------------ Queue variables are instance-specific ------------------ -q1 = Queue:new() -q2 = Queue:new() +q1 = Queue() +q2 = Queue() q1:Enqueue(17) @@ -33,8 +33,8 @@ assert(q1:Dequeue() == nil) assert(q2:Dequeue() == nil) -- ----------------------------- test WaitQueue ------------------------------ -q1 = WaitQueue:new() -q2 = WaitQueue:new() +q1 = WaitQueue() +q2 = WaitQueue() result = {} values = { 1, 1, 2, 3, 5, 8, 13, 21 } @@ -76,7 +76,7 @@ print('result:', inspect(result)) assert(util.equal(values, result)) -- try incrementally enqueueing values -q3 = WaitQueue:new() +q3 = WaitQueue() result = {} values = { 'This', 'is', 'a', 'test', 'script' } @@ -124,7 +124,7 @@ print(string.format('%q', table.concat(result, ' '))) assert(util.equal(values, result)) -- ----------------------------- test ErrorQueue ----------------------------- -q4 = ErrorQueue:new() +q4 = ErrorQueue() result = {} values = { 'This', 'is', 'a', 'test', 'script' } diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/require/ErrorQueue.lua index 13e4e92941..e6e9a5ef48 100644 --- a/indra/newview/scripts/lua/ErrorQueue.lua +++ b/indra/newview/scripts/lua/require/ErrorQueue.lua @@ -5,8 +5,11 @@ local WaitQueue = require('WaitQueue') local function dbg(...) end -- local dbg = require('printf') +local util = require('util') -local ErrorQueue = WaitQueue:new() +local ErrorQueue = WaitQueue() + +util.classctor(ErrorQueue) function ErrorQueue:Error(message) -- Setting Error() is a marker, like closing the queue. Once we reach the diff --git a/indra/newview/scripts/lua/Floater.lua b/indra/newview/scripts/lua/require/Floater.lua index 75696533e4..d057a74386 100644 --- a/indra/newview/scripts/lua/Floater.lua +++ b/indra/newview/scripts/lua/require/Floater.lua @@ -2,6 +2,7 @@ local leap = require 'leap' local fiber = require 'fiber' +local util = require 'util' -- list of all the events that a LLLuaFloater might send local event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"}).events @@ -45,6 +46,8 @@ function Floater:new(path, extra) return obj end +util.classctor(Floater) + function Floater:show() -- leap.eventstream() returns the first response, and launches a -- background fiber to call the passed callback with all subsequent diff --git a/indra/newview/scripts/lua/LLChat.lua b/indra/newview/scripts/lua/require/LLChat.lua index 7db538e837..78dca765e8 100644 --- a/indra/newview/scripts/lua/LLChat.lua +++ b/indra/newview/scripts/lua/require/LLChat.lua @@ -1,4 +1,4 @@ -leap = require 'leap' +local leap = require 'leap' local LLChat = {} diff --git a/indra/newview/scripts/lua/LLDebugSettings.lua b/indra/newview/scripts/lua/require/LLDebugSettings.lua index c250019a00..cff1a63c21 100644 --- a/indra/newview/scripts/lua/LLDebugSettings.lua +++ b/indra/newview/scripts/lua/require/LLDebugSettings.lua @@ -1,4 +1,4 @@ -leap = require 'leap' +local leap = require 'leap' local LLDebugSettings = {} diff --git a/indra/newview/scripts/lua/LLFloaterAbout.lua b/indra/newview/scripts/lua/require/LLFloaterAbout.lua index 44afee2e5c..a6e42d364f 100644 --- a/indra/newview/scripts/lua/LLFloaterAbout.lua +++ b/indra/newview/scripts/lua/require/LLFloaterAbout.lua @@ -1,6 +1,6 @@ -- Engage the LLFloaterAbout LLEventAPI -leap = require 'leap' +local leap = require 'leap' local LLFloaterAbout = {} diff --git a/indra/newview/scripts/lua/LLGesture.lua b/indra/newview/scripts/lua/require/LLGesture.lua index cb410446d7..343b611e2c 100644 --- a/indra/newview/scripts/lua/LLGesture.lua +++ b/indra/newview/scripts/lua/require/LLGesture.lua @@ -1,6 +1,6 @@ -- Engage the LLGesture LLEventAPI -leap = require 'leap' +local leap = require 'leap' local LLGesture = {} diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/require/Queue.lua index 5ab2a8a72c..5bc72e4057 100644 --- a/indra/newview/scripts/lua/Queue.lua +++ b/indra/newview/scripts/lua/require/Queue.lua @@ -7,6 +7,8 @@ -- But had to resist -- For fear it might be too obscua. +local util = require 'util' + local Queue = {} function Queue:new() @@ -20,6 +22,8 @@ function Queue:new() return obj end +util.classctor(Queue) + -- Check if the queue is empty function Queue:IsEmpty() return self._first > self._last diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua new file mode 100644 index 0000000000..eb1a4017c7 --- /dev/null +++ b/indra/newview/scripts/lua/require/UI.lua @@ -0,0 +1,125 @@ +-- Engage the viewer's UI + +local leap = require 'leap' +local Timer = (require 'timers').Timer +local mapargs = require 'mapargs' + +local UI = {} + +-- *************************************************************************** +-- registered menu actions +-- *************************************************************************** +function UI.call(func, parameter) + -- 'call' is fire-and-forget + leap.request('UI', {op='call', ['function']=func, parameter=parameter}) +end + +function UI.getValue(path) + return leap.request('UI', {op='getValue', path=path})['value'] +end + +-- *************************************************************************** +-- UI views +-- *************************************************************************** +-- Either: +-- wreq{op='Something', a=1, b=2, ...} +-- or: +-- (args should be local, as this wreq() call modifies it) +-- local args = {a=1, b=2, ...} +-- wreq('Something', args) +local function wreq(op_or_data, data_if_op) + if data_if_op ~= nil then + -- this is the wreq(op, data) form + data_if_op.op = op_or_data + op_or_data = data_if_op + end + return leap.request('LLWindow', op_or_data) +end + +-- omit 'parent' to list all view paths +function UI.listviews(parent) + return wreq{op='getPaths', under=parent} +end + +function UI.viewinfo(path) + return wreq{op='getInfo', path=path} +end + +-- *************************************************************************** +-- mouse actions +-- *************************************************************************** +-- pass a table: +-- UI.click{path=path +-- [, button='LEFT' | 'CENTER' | 'RIGHT'] +-- [, x=x, y=y] +-- [, hold=duration]} +function UI.click(...) + local args = mapargs('path,button,x,y,hold', ...) + args.button = args.button or 'LEFT' + local hold = args.hold or 1.0 + wreq('mouseMove', args) + wreq('mouseDown', args) + Timer(hold, 'wait') + wreq('mouseUp', args) +end + +-- pass a table as for UI.click() +function UI.doubleclick(...) + local args = mapargs('path,button,x,y', ...) + args.button = args.button or 'LEFT' + wreq('mouseDown', args) + wreq('mouseUp', args) + wreq('mouseDown', args) + wreq('mouseUp', args) +end + +-- UI.drag{path=, xoff=, yoff=} +function UI.drag(...) + local args = mapargs('path,xoff,yoff', ...) + -- query the specified path + local rect = UI.viewinfo(args.path).rect + local centerx = math.floor(rect.left + (rect.right - rect.left)/2) + local centery = math.floor(rect.bottom + (rect.top - rect.bottom)/2) + wreq{op='mouseMove', path=args.path, x=centerx, y=centery} + wreq{op='mouseDown', path=args.path, button='LEFT'} + wreq{op='mouseMove', path=args.path, x=centerx + args.xoff, y=centery + args.yoff} + wreq{op='mouseUp', path=args.path, button='LEFT'} +end + +-- *************************************************************************** +-- keyboard actions +-- *************************************************************************** +-- pass a table: +-- UI.keypress{ +-- [path=path] -- if omitted, default input field +-- [, char='x'] -- requires one of char, keycode, keysym +-- [, keycode=120] +-- keysym per https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124 +-- [, keysym='Enter'] +-- [, mask={'SHIFT', 'CTL', 'ALT', 'MAC_CONTROL'}] -- some subset of these +-- } +function UI.keypress(...) + local args = mapargs('path,char,keycode,keysym,mask', ...) + if args.char == '\n' then + args.char = nil + args.keysym = 'Enter' + end + return wreq('keyDown', args) +end + +-- UI.type{text=, path=} +function UI.type(...) + local args = mapargs('text,path', ...) + if #args.text > 0 then + -- The caller's path may be specified in a way that requires recursively + -- searching parts of the LLView tree. No point in doing that more than + -- once. Capture the actual path found by that first call and use that for + -- subsequent calls. + local path = UI.keypress{path=args.path, char=string.sub(args.text, 1, 1)}.path + for i = 2, #args.text do + UI.keypress{path=path, char=string.sub(args.text, i, i)} + end + end +end + +return UI diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/require/WaitQueue.lua index 6bcb9d62c2..7e10d03295 100644 --- a/indra/newview/scripts/lua/WaitQueue.lua +++ b/indra/newview/scripts/lua/require/WaitQueue.lua @@ -4,14 +4,15 @@ local fiber = require('fiber') local Queue = require('Queue') +local util = require('util') local function dbg(...) end -- local dbg = require('printf') -local WaitQueue = Queue:new() +local WaitQueue = Queue() function WaitQueue:new() - local obj = Queue:new() + local obj = Queue() setmetatable(obj, self) self.__index = self @@ -20,6 +21,8 @@ function WaitQueue:new() return obj end +util.classctor(WaitQueue) + function WaitQueue:Enqueue(value) if self._closed then error("can't Enqueue() on closed Queue") diff --git a/indra/newview/scripts/lua/coro.lua b/indra/newview/scripts/lua/require/coro.lua index 616a797e95..616a797e95 100644 --- a/indra/newview/scripts/lua/coro.lua +++ b/indra/newview/scripts/lua/require/coro.lua diff --git a/indra/newview/scripts/lua/fiber.lua b/indra/newview/scripts/lua/require/fiber.lua index cae27b936b..cae27b936b 100644 --- a/indra/newview/scripts/lua/fiber.lua +++ b/indra/newview/scripts/lua/require/fiber.lua diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/require/inspect.lua index 9900a0b81b..9900a0b81b 100644 --- a/indra/newview/scripts/lua/inspect.lua +++ b/indra/newview/scripts/lua/require/inspect.lua diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/require/leap.lua index 8caae24e94..82f91ce9e9 100644 --- a/indra/newview/scripts/lua/leap.lua +++ b/indra/newview/scripts/lua/require/leap.lua @@ -43,6 +43,7 @@ local ErrorQueue = require('ErrorQueue') local inspect = require('inspect') local function dbg(...) end -- local dbg = require('printf') +local util = require('util') local leap = {} @@ -129,7 +130,7 @@ local function requestSetup(pump, data) -- because, unlike the WaitFor base class, WaitForReqid does not -- self-register on our waitfors list. Instead, capture the new -- WaitForReqid object in pending so dispatch() can find it. - local waitfor = leap.WaitForReqid:new(reqid) + local waitfor = leap.WaitForReqid(reqid) pending[reqid] = waitfor -- Pass reqid to send() to stamp it into (a copy of) the request data. dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name) @@ -432,7 +433,7 @@ function leap.WaitFor:new(priority, name) self._id += 1 obj.name = 'WaitFor' .. self._id end - obj._queue = ErrorQueue:new() + obj._queue = ErrorQueue() obj._registered = false -- if no priority, then don't enable() - remember 0 is truthy if priority then @@ -442,6 +443,8 @@ function leap.WaitFor:new(priority, name) return obj end +util.classctor(leap.WaitFor) + -- Re-enable a disable()d WaitFor object. New WaitFor objects are -- enable()d by default. function leap.WaitFor:enable() @@ -514,13 +517,13 @@ function leap.WaitFor:exception(message) end -- ------------------------------ WaitForReqid ------------------------------- -leap.WaitForReqid = leap.WaitFor:new() +leap.WaitForReqid = leap.WaitFor() function leap.WaitForReqid:new(reqid) -- priority is meaningless, since this object won't be added to the -- priority-sorted waitfors list. Use the reqid as the debugging name -- string. - local obj = leap.WaitFor:new(nil, 'WaitForReqid(' .. reqid .. ')') + local obj = leap.WaitFor(nil, 'WaitForReqid(' .. reqid .. ')') setmetatable(obj, self) self.__index = self @@ -529,6 +532,8 @@ function leap.WaitForReqid:new(reqid) return obj end +util.classctor(leap.WaitForReqid) + function leap.WaitForReqid:filter(pump, data) -- Because we expect to directly look up the WaitForReqid object of -- interest based on the incoming ["reqid"] value, it's not necessary diff --git a/indra/newview/scripts/lua/require/popup.lua b/indra/newview/scripts/lua/require/popup.lua new file mode 100644 index 0000000000..3aaadf85ba --- /dev/null +++ b/indra/newview/scripts/lua/require/popup.lua @@ -0,0 +1,53 @@ +local leap = require 'leap' +local mapargs = require 'mapargs' + +-- notification is any name defined in notifications.xml as +-- <notification name=> +-- vars is a table providing values for [VAR] substitution keys in the +-- notification body +-- payload prepopulates the response table +-- wait=false means fire and forget, otherwise wait for user response +local popup_meta = { + -- setting this function as getmetatable(popup).__call() means this gets + -- called when a consumer calls popup(notification, vars, payload) + __call = function(self, ...) + local args = mapargs('notification,vars,payload,wait', ...) + -- we use convenience argument names different from 'LLNotifications' + -- listener + args.name = args.notification + args.notification = nil + args.substitutions = args.vars + args.vars = nil + local wait = args.wait + args.wait = nil + args.op = 'requestAdd' + -- Specifically test (wait == false), NOT (not wait), because we treat + -- nil (omitted, default true) differently than false (explicitly + -- DON'T wait). + if wait == false then + leap.send('LLNotifications', args) + else + return leap.request('LLNotifications', args).response + end + end +} + +local popup = setmetatable({}, popup_meta) + +function popup:alert(message) + return self('GenericAlert', {MESSAGE=message}) +end + +function popup:alertOK(message) + return self('GenericAlertOK', {MESSAGE=message}) +end + +function popup:alertYesCancel(message) + return self('GenericAlertYesCancel', {MESSAGE=message}) +end + +function popup:tip(message) + self{'SystemMessageTip', {MESSAGE=message}, wait=false} +end + +return popup diff --git a/indra/newview/scripts/lua/printf.lua b/indra/newview/scripts/lua/require/printf.lua index e84b2024df..e84b2024df 100644 --- a/indra/newview/scripts/lua/printf.lua +++ b/indra/newview/scripts/lua/require/printf.lua diff --git a/indra/newview/scripts/lua/startup.lua b/indra/newview/scripts/lua/require/startup.lua index 4311bb9a60..c3040f94b8 100644 --- a/indra/newview/scripts/lua/startup.lua +++ b/indra/newview/scripts/lua/require/startup.lua @@ -12,6 +12,8 @@ local function dbg(...) end -- local dbg = require 'printf' -- --------------------------------------------------------------------------- +local startup = {} + -- Get the list of startup states from the viewer. local bynum = leap.request('LLStartUp', {op='getStateTable'})['table'] @@ -19,7 +21,7 @@ local byname = setmetatable( {}, -- set metatable to throw an error if you look up invalid state name {__index=function(t, k) - local v = t[k] + local v = rawget(t, k) if v then return v end @@ -35,7 +37,7 @@ end -- specialize a WaitFor to track the viewer's startup state local startup_pump = 'StartupState' -local waitfor = leap.WaitFor:new(0, startup_pump) +local waitfor = leap.WaitFor(0, startup_pump) function waitfor:filter(pump, data) if pump == self.name then return data @@ -57,8 +59,6 @@ leap.request(leap.cmdpump(), leap.send('LLStartUp', {op='postStartupState'}) -- --------------------------------------------------------------------------- -startup = {} - -- wait for response from postStartupState while not startup._state do dbg('startup.state() waiting for first StartupState event') @@ -98,4 +98,3 @@ function startup.wait(state) end return startup - diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/require/timers.lua index e0d27a680d..e4938078dc 100644 --- a/indra/newview/scripts/lua/timers.lua +++ b/indra/newview/scripts/lua/require/timers.lua @@ -1,6 +1,7 @@ -- Access to the viewer's time-delay facilities local leap = require 'leap' +local util = require 'util' local timers = {} @@ -78,6 +79,8 @@ function timers.Timer:new(delay, callback, iterate) return obj end +util.classctor(timers.Timer) + function timers.Timer:tick() error('Pass a callback to Timer:new(), or override Timer:tick()') end diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/require/util.lua index a2191288f6..bfbfc8637c 100644 --- a/indra/newview/scripts/lua/util.lua +++ b/indra/newview/scripts/lua/require/util.lua @@ -2,6 +2,31 @@ local util = {} +-- Allow MyClass(ctor args...) equivalent to MyClass:new(ctor args...) +-- Usage: +-- local MyClass = {} +-- function MyClass:new(...) +-- ... +-- end +-- ... +-- util.classctor(MyClass) +-- or if your constructor is named something other than MyClass:new(), e.g. +-- MyClass:construct(): +-- util.classctor(MyClass, MyClass.construct) +-- return MyClass +function util.classctor(class, ctor) + -- get the metatable for the passed class + local mt = getmetatable(class) + if mt == nil then + -- if it doesn't already have a metatable, then create one + mt = {} + setmetatable(class, mt) + end + -- now that class has a metatable, set its __call method to the specified + -- constructor method (class.new if not specified) + mt.__call = ctor or class.new +end + -- check if array-like table contains certain value function util.contains(t, v) return table.find(t, v) ~= nil diff --git a/indra/newview/scripts/lua/test_login.lua b/indra/newview/scripts/lua/test_login.lua new file mode 100644 index 0000000000..6df52b08c2 --- /dev/null +++ b/indra/newview/scripts/lua/test_login.lua @@ -0,0 +1,7 @@ +startup = require 'startup' +login = require 'login' + +startup.wait('STATE_LOGIN_WAIT') +login() +-- WIP: not working as of 2024-06-11 +-- login('My Username', 'password') diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua index ab638dcdd1..65a31670c8 100644 --- a/indra/newview/scripts/lua/test_luafloater_demo.lua +++ b/indra/newview/scripts/lua/test_luafloater_demo.lua @@ -60,7 +60,7 @@ local resp = leap.request("LLFloaterReg", key) COMMAND_PUMP_NAME = resp.command_name reqid = resp.reqid -catch_events = leap.WaitFor:new(-1, "all_events") +catch_events = leap.WaitFor(-1, "all_events") function catch_events:filter(pump, data) if data.reqid == reqid then return data diff --git a/indra/newview/scripts/lua/test_luafloater_demo2.lua b/indra/newview/scripts/lua/test_luafloater_demo2.lua index 9e24237d28..3903d01e65 100644 --- a/indra/newview/scripts/lua/test_luafloater_demo2.lua +++ b/indra/newview/scripts/lua/test_luafloater_demo2.lua @@ -2,7 +2,7 @@ local Floater = require 'Floater' local leap = require 'leap' local startup = require 'startup' -local flt = Floater:new( +local flt = Floater( 'luafloater_demo.xml', {show_time_lbl = {"right_mouse_down", "double_click"}}) diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua index 3d9a9b0ad4..a5fd325430 100644 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua @@ -58,7 +58,7 @@ local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"} key.extra_events={gesture_list = {_event("double_click")}} handleEvents(leap.request("LLFloaterReg", key)) -catch_events = leap.WaitFor:new(-1, "all_events") +catch_events = leap.WaitFor(-1, "all_events") function catch_events:filter(pump, data) if data.reqid == reqid then return data diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua index d702d09c51..bd397ef2a6 100644 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua @@ -2,7 +2,7 @@ local Floater = require 'Floater' local LLGesture = require 'LLGesture' local startup = require 'startup' -local flt = Floater:new( +local flt = Floater( "luafloater_gesture_list.xml", {gesture_list = {"double_click"}}) diff --git a/indra/newview/scripts/lua/test_luafloater_speedometer.lua b/indra/newview/scripts/lua/test_luafloater_speedometer.lua index a9d3a70330..af7189a2cb 100644 --- a/indra/newview/scripts/lua/test_luafloater_speedometer.lua +++ b/indra/newview/scripts/lua/test_luafloater_speedometer.lua @@ -1,10 +1,10 @@ local Floater = require 'Floater' local leap = require 'leap' -local LLNotification = require 'LLNotification' +local popup = require 'popup' local startup = require 'startup' local Timer = (require 'timers').Timer local max_speed = 0 -local flt = Floater:new("luafloater_speedometer.xml") +local flt = Floater("luafloater_speedometer.xml") startup.wait('STATE_STARTED') local timer @@ -13,8 +13,7 @@ function flt:floater_close(event_data) if timer then timer:cancel() end - msg = "Registered max speed: " .. string.format("%.2f", max_speed) .. " m/s"; - LLNotification.add('SystemMessageTip', {MESSAGE = msg}) + popup:tip(string.format("Registered max speed: %.2f m/s", max_speed)) end local function idle(event_data) @@ -24,9 +23,9 @@ local function idle(event_data) end msg = 'Are you sure you want to run this "speedometer" script?' -response = LLNotification.requestAdd('GenericAlertYesCancel', {MESSAGE = msg}) +response = popup:alertYesCancel(msg) if response.OK_okcancelbuttons then flt:show() - timer = Timer:new(1, idle, true) -- iterate + timer = Timer(1, idle, true) -- iterate end diff --git a/indra/newview/scripts/lua/test_mapargs.lua b/indra/newview/scripts/lua/test_mapargs.lua new file mode 100644 index 0000000000..999a57acb4 --- /dev/null +++ b/indra/newview/scripts/lua/test_mapargs.lua @@ -0,0 +1,68 @@ +local mapargs = require 'mapargs' +local inspect = require 'inspect' + +function tabfunc(...) + local a = mapargs({'a1', 'a2', 'a3'}, ...) + print(inspect(a)) +end + +print('----------') +print('f(10, 20, 30)') +tabfunc(10, 20, 30) +print('f(10, nil, 30)') +tabfunc(10, nil, 30) +print('f{10, 20, 30}') +tabfunc{10, 20, 30} +print('f{10, nil, 30}') +tabfunc{10, nil, 30} +print('f{a3=300, a1=100}') +tabfunc{a3=300, a1=100} +print('f{1, a3=3}') +tabfunc{1, a3=3} +print('f{a3=3, 1}') +tabfunc{a3=3, 1} +print('----------') + +if false then + -- the code below was used to explore ideas that became mapargs() + mixed = { '[1]', nil, '[3]', abc='[abc]', '[3]', def='[def]' } + local function showtable(desc, t) + print(string.format('%s (len %s)\n%s', desc, #t, inspect(t))) + end + showtable('mixed', mixed) + + print('ipairs(mixed)') + for k, v in ipairs(mixed) do + print(string.format('[%s] = %s', k, tostring(v))) + end + + print('table.pack(mixed)') + print(inspect(table.pack(mixed))) + + local function nilarg(desc, a, b, c) + print(desc) + print('a = ' .. tostring(a)) + print('b = ' .. tostring(b)) + print('c = ' .. tostring(c)) + end + + nilarg('nilarg(1)', 1) + nilarg('nilarg(1, nil, 3)', 1, nil, 3) + + local function nilargs(desc, ...) + args = table.pack(...) + showtable(desc, args) + end + + nilargs('nilargs{a=1, b=2, c=3}', {a=1, b=2, c=3}) + nilargs('nilargs(1, 2, 3)', 1, 2, 3) + nilargs('nilargs(1, nil, 3)', 1, nil, 3) + nilargs('nilargs{1, 2, 3}', {1, 2, 3}) + nilargs('nilargs{1, nil, 3}', {1, nil, 3}) + + print('table.unpack({1, nil, 3})') + a, b, c = table.unpack({1, nil, 3}) + print('a = ' .. tostring(a)) + print('b = ' .. tostring(b)) + print('c = ' .. tostring(c)) +end diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua index ed0de070f7..be5001aa16 100644 --- a/indra/newview/scripts/lua/test_timers.lua +++ b/indra/newview/scripts/lua/test_timers.lua @@ -6,9 +6,9 @@ local timers = require 'timers' -- 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)') +print('t0(10)') start = os.clock() -t0 = timers.Timer:new(10, function() print('t0 fired at', os.clock() - start) end) +t0 = timers.Timer(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()) @@ -18,16 +18,16 @@ 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)') +print('t1(5)') start = os.clock() -t1 = timers.Timer:new(5, function() print('t1 fired at', os.clock() - start) end) +t1 = timers.Timer(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)') +print('t2(2)') start = os.clock() -t2 = timers.Timer:new(2) +t2 = timers.Timer(2) function t2:tick() print('t2 fired at', os.clock() - start) end @@ -37,7 +37,7 @@ end -- then the t1 callback message before the Timer(5) completion message. print('Timer(5) waiting') start = os.clock() -timers.Timer:new(5, 'wait') +timers.Timer(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 @@ -50,7 +50,7 @@ print(string.format('Timer(5) waited %f seconds', os.clock() - start)) -- it's worth knowing that a coroutine timer callback can be used to manage -- more complex control flows. start = os.clock() -timers.Timer:new( +timers.Timer( 2, coroutine.wrap(function() for i = 1,5 do diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index cf1bf25b5c..2d525f7913 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -398,7 +398,7 @@ namespace tut LuaState L; auto future = LLLUAmanager::startScriptLine(L, lua); - auto replyname{ L.obtainListener()->getReplyName() }; + auto replyname{ L.obtainListener().getReplyName() }; auto& replypump{ LLEventPumps::instance().obtain(replyname) }; // LuaState::expr() periodically interrupts a running chunk to ensure // the rest of our coroutines get cycles. Nonetheless, for this test diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 53039fbd99..20dced2341 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -169,6 +169,8 @@ class ViewerManifest(LLManifest): with self.prefix(src_dst="scripts/lua"): self.path("*.lua") self.path("*.xml") + with self.prefix(src_dst='require'): + self.path("*.lua") #build_data.json. Standard with exception handling is fine. If we can't open a new file for writing, we have worse problems #platform is computed above with other arg parsing @@ -285,7 +287,7 @@ class ViewerManifest(LLManifest): # A line that starts with a non-whitespace character is a name; all others describe contributions, so collect the names names = [] for line in lines : - if re.match("\S", line) : + if re.match(r"\S", line) : names.append(line.rstrip()) # It's not fair to always put the same people at the head of the list random.shuffle(names) |