summaryrefslogtreecommitdiff
path: root/indra/newview/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview/scripts')
-rw-r--r--indra/newview/scripts/lua/frame_profile.lua24
-rw-r--r--indra/newview/scripts/lua/frame_profile_quit.lua34
-rw-r--r--indra/newview/scripts/lua/luafloater_camera_control.xml244
-rw-r--r--indra/newview/scripts/lua/luafloater_demo.xml93
-rw-r--r--indra/newview/scripts/lua/luafloater_gesture_list.xml21
-rw-r--r--indra/newview/scripts/lua/luafloater_outfits_list.xml51
-rw-r--r--indra/newview/scripts/lua/luafloater_speedometer.xml35
-rw-r--r--indra/newview/scripts/lua/qtest.lua146
-rw-r--r--indra/newview/scripts/lua/require/ErrorQueue.lua37
-rw-r--r--indra/newview/scripts/lua/require/LLAgent.lua143
-rw-r--r--indra/newview/scripts/lua/require/LLAppearance.lua31
-rw-r--r--indra/newview/scripts/lua/require/LLChat.lua39
-rw-r--r--indra/newview/scripts/lua/require/LLDebugSettings.lua17
-rw-r--r--indra/newview/scripts/lua/require/LLFloaterAbout.lua11
-rw-r--r--indra/newview/scripts/lua/require/LLGesture.lua23
-rw-r--r--indra/newview/scripts/lua/require/LLInventory.lua67
-rw-r--r--indra/newview/scripts/lua/require/LLListener.lua50
-rw-r--r--indra/newview/scripts/lua/require/Queue.lua51
-rw-r--r--indra/newview/scripts/lua/require/Region.lua17
-rw-r--r--indra/newview/scripts/lua/require/UI.lua243
-rw-r--r--indra/newview/scripts/lua/require/UI/Floater.lua146
-rw-r--r--indra/newview/scripts/lua/require/UI/popup.lua82
-rw-r--r--indra/newview/scripts/lua/require/WaitQueue.lua88
-rw-r--r--indra/newview/scripts/lua/require/coro.lua67
-rw-r--r--indra/newview/scripts/lua/require/fiber.lua346
-rw-r--r--indra/newview/scripts/lua/require/inspect.lua371
-rw-r--r--indra/newview/scripts/lua/require/leap.lua550
-rw-r--r--indra/newview/scripts/lua/require/login.lua42
-rw-r--r--indra/newview/scripts/lua/require/logout.lua7
-rw-r--r--indra/newview/scripts/lua/require/mapargs.lua73
-rw-r--r--indra/newview/scripts/lua/require/printf.lua19
-rw-r--r--indra/newview/scripts/lua/require/result_view.lua98
-rw-r--r--indra/newview/scripts/lua/require/startup.lua100
-rw-r--r--indra/newview/scripts/lua/require/timers.lua122
-rw-r--r--indra/newview/scripts/lua/require/util.lua114
-rw-r--r--indra/newview/scripts/lua/test_LLAppearance.lua114
-rw-r--r--indra/newview/scripts/lua/test_LLChat.lua18
-rw-r--r--indra/newview/scripts/lua/test_LLChatListener.lua39
-rw-r--r--indra/newview/scripts/lua/test_LLFloaterAbout.lua6
-rw-r--r--indra/newview/scripts/lua/test_LLGesture.lua26
-rw-r--r--indra/newview/scripts/lua/test_LLInventory.lua24
-rw-r--r--indra/newview/scripts/lua/test_animation.lua32
-rw-r--r--indra/newview/scripts/lua/test_atexit.lua3
-rw-r--r--indra/newview/scripts/lua/test_autopilot.lua22
-rw-r--r--indra/newview/scripts/lua/test_callables.lua6
-rw-r--r--indra/newview/scripts/lua/test_camera_control.lua49
-rw-r--r--indra/newview/scripts/lua/test_flycam.lua38
-rw-r--r--indra/newview/scripts/lua/test_group_chat.lua14
-rw-r--r--indra/newview/scripts/lua/test_inv_resultset.lua18
-rw-r--r--indra/newview/scripts/lua/test_login.lua10
-rw-r--r--indra/newview/scripts/lua/test_logout.lua3
-rw-r--r--indra/newview/scripts/lua/test_luafloater_demo.lua39
-rw-r--r--indra/newview/scripts/lua/test_luafloater_gesture_list.lua27
-rw-r--r--indra/newview/scripts/lua/test_luafloater_speedometer.lua31
-rw-r--r--indra/newview/scripts/lua/test_mapargs.lua68
-rw-r--r--indra/newview/scripts/lua/test_popup.lua10
-rw-r--r--indra/newview/scripts/lua/test_result_view.lua55
-rw-r--r--indra/newview/scripts/lua/test_setdtor.lua91
-rw-r--r--indra/newview/scripts/lua/test_snapshot.lua15
-rw-r--r--indra/newview/scripts/lua/test_timers.lua63
-rw-r--r--indra/newview/scripts/lua/test_toolbars.lua26
-rw-r--r--indra/newview/scripts/lua/test_top_menu.lua52
-rw-r--r--indra/newview/scripts/lua/testmod.lua2
63 files changed, 4503 insertions, 0 deletions
diff --git a/indra/newview/scripts/lua/frame_profile.lua b/indra/newview/scripts/lua/frame_profile.lua
new file mode 100644
index 0000000000..3c6353ff68
--- /dev/null
+++ b/indra/newview/scripts/lua/frame_profile.lua
@@ -0,0 +1,24 @@
+-- Trigger Develop -> Render Tests -> Frame Profile
+
+LLAgent = require 'LLAgent'
+startup = require 'startup'
+Timer = (require 'timers').Timer
+UI = require 'UI'
+
+startup.wait('STATE_STARTED')
+
+-- teleport to http://maps.secondlife.com/secondlife/Bug%20Island/220/224/27
+print(LLAgent.teleport{regionname='Bug Island', x=220, y=224, z=27})
+Timer(10, 'wait')
+LLAgent.setCamera{camera_pos={220, 224, 26}, camera_locked=true,
+ focus_pos ={228, 232, 26}, focus_locked=true}
+Timer(1, 'wait')
+-- This freezes the viewer for perceptible realtime
+UI.popup:tip('starting Render Tests -> Frame Profile')
+UI.call("Advanced.ClickRenderProfile")
+Timer(1, 'wait')
+LLAgent.removeCamParams()
+LLAgent.setFollowCamActive(false)
+
+-- Home, James!
+print(LLAgent.teleport('home'))
diff --git a/indra/newview/scripts/lua/frame_profile_quit.lua b/indra/newview/scripts/lua/frame_profile_quit.lua
new file mode 100644
index 0000000000..ad136c86d2
--- /dev/null
+++ b/indra/newview/scripts/lua/frame_profile_quit.lua
@@ -0,0 +1,34 @@
+-- Trigger Develop -> Render Tests -> Frame Profile and quit
+
+assert(arg.n == 3, 'Usage: frame_profile_quit.lua x y z (for camera focus)')
+-- Try coercing string arguments to numbers, using Lua's implicit conversion.
+-- If the args passed as x, y, z won't convert nicely to numbers, better find
+-- out now.
+focus = {arg[1]+0, arg[2]+0, arg[3]+0}
+
+LLAgent = require 'LLAgent'
+logout = require 'logout'
+startup = require 'startup'
+Timer = (require 'timers').Timer
+UI = require 'UI'
+
+startup.wait('STATE_STARTED')
+
+-- Figure out where we are
+camera = LLAgent.getRegionPosition()
+-- assume z is the agent's feet, add 2 meters
+camera[2] += 2
+
+Timer(10, 'wait')
+LLAgent.setCamera{camera_pos=camera, camera_locked=true,
+ focus_pos=focus, focus_locked=true}
+Timer(1, 'wait')
+-- This freezes the viewer for perceptible realtime
+UI.popup:tip('starting Render Tests -> Frame Profile')
+UI.call("Advanced.ClickRenderProfile")
+Timer(1, 'wait')
+LLAgent.removeCamParams()
+LLAgent.setFollowCamActive(false)
+
+-- done
+logout()
diff --git a/indra/newview/scripts/lua/luafloater_camera_control.xml b/indra/newview/scripts/lua/luafloater_camera_control.xml
new file mode 100644
index 0000000000..0601a363e5
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_camera_control.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="350"
+ layout="topleft"
+ name="camera_demo"
+ title="Follow camera control"
+ width="360">
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="100"
+ layout="topleft"
+ top="30"
+ left="10"
+ name="camera_lbl">
+ Camera position:
+ </text>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="10"
+ layout="topleft"
+ top_pad="12"
+ left="10"
+ value="X"
+ name="cam_x_lbl"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="cam_x"
+ top_delta="-2"
+ left_pad="5"
+ width="65" />
+ <button
+ follows="left|bottom"
+ height="20"
+ image_overlay="Icon_Copy"
+ image_selected="Toolbar_Middle_Selected"
+ image_unselected="Toolbar_Middle_Off"
+ layout="topleft"
+ tool_tip="Use Agent position"
+ name="agent_cam_btn"
+ left_pad="10"
+ width="20" >
+ </button>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="10"
+ layout="topleft"
+ top_pad="7"
+ left="10"
+ value="Y"
+ name="cam_y_lbl"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="cam_y"
+ top_delta="-2"
+ left_pad="5"
+ width="65" />
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="10"
+ layout="topleft"
+ top_pad="7"
+ left="10"
+ value="Z"
+ name="cam_z_lbl"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="cam_z"
+ top_delta="-2"
+ left_pad="5"
+ width="65" />
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="100"
+ layout="topleft"
+ top="30"
+ left="145"
+ name="focus_lbl">
+ Focus position:
+ </text>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="10"
+ layout="topleft"
+ top_pad="12"
+ left="145"
+ value="X"
+ name="cam_x_lbl"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="focus_x"
+ top_delta="-2"
+ left_pad="5"
+ width="60" />
+ <button
+ follows="left|bottom"
+ height="20"
+ image_overlay="Icon_Copy"
+ image_selected="Toolbar_Middle_Selected"
+ image_unselected="Toolbar_Middle_Off"
+ layout="topleft"
+ tool_tip="Use Agent position"
+ name="agent_focus_btn"
+ left_pad="10"
+ width="20" >
+ </button>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="10"
+ layout="topleft"
+ top_pad="7"
+ left="145"
+ value="Y"
+ name="cam_y_lbl"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="focus_y"
+ top_delta="-2"
+ left_pad="5"
+ width="60" />
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="10"
+ layout="topleft"
+ top_pad="7"
+ left="145"
+ value="Z"
+ name="cam_z_lbl"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="focus_z"
+ top_delta="-2"
+ left_pad="5"
+ width="60" />
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ width="100"
+ layout="topleft"
+ top="30"
+ left="270"
+ name="focus_lbl">
+ Lock:
+ </text>
+ <check_box
+ width="80"
+ height="21"
+ layout="topleft"
+ follows="top|left"
+ top_pad="12"
+ label="Camera"
+ name="lock_camera_ctrl"/>
+ <check_box
+ width="80"
+ height="21"
+ layout="topleft"
+ follows="top|left"
+ top_pad="2"
+ label="Focus"
+ name="lock_focus_ctrl"/>
+ <button
+ follows="left|bottom"
+ height="25"
+ label="Update params"
+ layout="topleft"
+ name="update_btn"
+ left="10"
+ top="140"
+ width="120" >
+ </button>
+ <button
+ follows="left|bottom"
+ height="25"
+ label="Reset"
+ layout="topleft"
+ name="reset_btn"
+ left_pad="15"
+ width="90" >
+ </button>
+ <text_editor
+ follows="top|left"
+ font="SansSerif"
+ height="160"
+ left="5"
+ enabled="false"
+ name="events_editor"
+ top_pad="15"
+ word_wrap="true"
+ max_length="65536"
+ width="350"/>
+</floater>
diff --git a/indra/newview/scripts/lua/luafloater_demo.xml b/indra/newview/scripts/lua/luafloater_demo.xml
new file mode 100644
index 0000000000..b2273d7718
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_demo.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="300"
+ layout="topleft"
+ name="lua_demo"
+ title="LUA"
+ width="320">
+ <check_box
+ height="16"
+ label="Disable button"
+ layout="topleft"
+ top_pad="35"
+ left="5"
+ name="disable_ctrl"
+ width="146" />
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="openfloater_cmd"
+ top_delta="25"
+ width="100" />
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Open floater"
+ layout="topleft"
+ name="open_btn"
+ top_delta="25"
+ width="100" >
+ </button>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ layout="topleft"
+ top="30"
+ left_delta="170"
+ name="title_lbl">
+ Select title
+ </text>
+ <combo_box
+ follows="top|left"
+ height="23"
+ name="title_cmb"
+ top_pad="5"
+ width="100">
+ <combo_box.item
+ label="LUA"
+ value="LUA" />
+ <combo_box.item
+ label="LUA floater"
+ value="LUA floater" />
+ <combo_box.item
+ label="LUA-U title"
+ value="LUA-U title" />
+ </combo_box>
+ <text
+ type="string"
+ follows="left|bottom"
+ height="10"
+ layout="topleft"
+ top_delta="50"
+ name="show_time_lbl">
+ Double click me
+ </text>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ layout="topleft"
+ top_pad="15"
+ font="SansSerif"
+ text_color="white"
+ left_delta="15"
+ name="time_lbl"/>
+ <text_editor
+ follows="top|left"
+ font="SansSerif"
+ height="140"
+ left="5"
+ enabled="false"
+ name="events_editor"
+ top_pad="15"
+ word_wrap="true"
+ max_length="65536"
+ width="310"/>
+</floater>
diff --git a/indra/newview/scripts/lua/luafloater_gesture_list.xml b/indra/newview/scripts/lua/luafloater_gesture_list.xml
new file mode 100644
index 0000000000..a38a04eed0
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_gesture_list.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="150"
+ layout="topleft"
+ name="lua_gestures"
+ title="Gestures"
+ width="320">
+ <scroll_list
+ draw_heading="false"
+ left="5"
+ width="310"
+ height="115"
+ top_pad ="25"
+ follows="all"
+ name="gesture_list">
+ <scroll_list.columns
+ name="gesture_name"
+ label="Name"/>
+ </scroll_list>
+</floater>
diff --git a/indra/newview/scripts/lua/luafloater_outfits_list.xml b/indra/newview/scripts/lua/luafloater_outfits_list.xml
new file mode 100644
index 0000000000..8cab864308
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_outfits_list.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="205"
+ layout="topleft"
+ name="lua_outfits"
+ title="Outfits"
+ width="325">
+ <scroll_list
+ draw_heading="false"
+ left="5"
+ width="315"
+ height="150"
+ top_pad ="25"
+ follows="all"
+ name="outfits_list">
+ <scroll_list.columns
+ name="outfit_name"
+ label="Name"/>
+ </scroll_list>
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Replace COF"
+ layout="topleft"
+ name="replace_btn"
+ enabled="false"
+ top_pad="5"
+ width="95" >
+ </button>
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Add to COF"
+ left_pad="7"
+ layout="topleft"
+ name="add_btn"
+ enabled="false"
+ width="95" >
+ </button>
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Show wearables"
+ left_pad="7"
+ layout="topleft"
+ name="select_btn"
+ enabled="false"
+ width="111" >
+ </button>
+</floater>
diff --git a/indra/newview/scripts/lua/luafloater_speedometer.xml b/indra/newview/scripts/lua/luafloater_speedometer.xml
new file mode 100644
index 0000000000..54b99c7d48
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_speedometer.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="60"
+ layout="topleft"
+ name="lua_speedometer"
+ title="Speedometer"
+ can_minimize="false"
+ width="170">
+ <text
+ type="string"
+ follows="left|top"
+ font="SansSerifBold"
+ font.size="Huge"
+ text_color="White"
+ layout="topleft"
+ top="25"
+ left_delta="30"
+ width="70"
+ height="30"
+ name="speed_lbl"/>
+ <text
+ type="string"
+ follows="left|top"
+ width="55"
+ height="30"
+ font="SansSerifBold"
+ font.size="Huge"
+ text_color="White"
+ layout="topleft"
+ left_pad="2"
+ name="mps_lbl">
+ m/s
+ </text>
+</floater>
diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua
new file mode 100644
index 0000000000..9526f58b04
--- /dev/null
+++ b/indra/newview/scripts/lua/qtest.lua
@@ -0,0 +1,146 @@
+-- Exercise the Queue, WaitQueue, ErrorQueue family
+
+Queue = require('Queue')
+WaitQueue = require('WaitQueue')
+ErrorQueue = require('ErrorQueue')
+util = require('util')
+
+inspect = require('inspect')
+
+-- resume() wrapper to propagate errors
+function resume(co, ...)
+ -- if there's an idiom other than table.pack() to assign an arbitrary
+ -- number of return values, I don't yet know it
+ local ok_result = table.pack(coroutine.resume(co, ...))
+ if not ok_result[1] then
+ -- if [1] is false, then [2] is the error message
+ error(ok_result[2])
+ end
+ -- ok is true, whew, just return the rest of the values
+ return table.unpack(ok_result, 2)
+end
+
+-- ------------------ Queue variables are instance-specific ------------------
+q1 = Queue()
+q2 = Queue()
+
+q1:Enqueue(17)
+
+assert(not q1:IsEmpty())
+assert(q2:IsEmpty())
+assert(q1:Dequeue() == 17)
+assert(q1:Dequeue() == nil)
+assert(q2:Dequeue() == nil)
+
+-- ----------------------------- test WaitQueue ------------------------------
+q1 = WaitQueue()
+q2 = WaitQueue()
+result = {}
+values = { 1, 1, 2, 3, 5, 8, 13, 21 }
+
+for i, value in pairs(values) do
+ q1:Enqueue(value)
+end
+-- close() while not empty tests that queue drains before reporting done
+q1:close()
+
+-- ensure that WaitQueue instance variables are in fact independent
+assert(q2:IsEmpty())
+
+-- consumer() coroutine to pull from the passed q until closed
+function consumer(desc, q)
+ print(string.format('consumer(%s) start', desc))
+ local value = q:Dequeue()
+ while value ~= nil do
+ print(string.format('consumer(%s) got %q', desc, value))
+ table.insert(result, value)
+ value = q:Dequeue()
+ end
+ print(string.format('consumer(%s) done', desc))
+end
+
+-- run two consumers
+coa = coroutine.create(consumer)
+cob = coroutine.create(consumer)
+-- Since consumer() doesn't yield while it can still retrieve values,
+-- consumer(a) will dequeue all values from q1 and return when done.
+resume(coa, 'a', q1)
+-- consumer(b) will wake up to find the queue empty and closed.
+resume(cob, 'b', q1)
+coroutine.close(coa)
+coroutine.close(cob)
+
+print('values:', inspect(values))
+print('result:', inspect(result))
+
+assert(util.equal(values, result))
+
+-- try incrementally enqueueing values
+q3 = WaitQueue()
+result = {}
+values = { 'This', 'is', 'a', 'test', 'script' }
+
+coros = {}
+for _, name in {'a', 'b'} do
+ local coro = coroutine.create(consumer)
+ table.insert(coros, coro)
+ -- Resuming both coroutines should leave them both waiting for a queue item.
+ resume(coro, name, q3)
+end
+
+for _, s in pairs(values) do
+ print(string.format('Enqueue(%q)', s))
+ q3:Enqueue(s)
+end
+q3:close()
+
+function joinall(coros)
+ local running
+ local errors = 0
+ repeat
+ running = false
+ for i, coro in pairs(coros) do
+ if coroutine.status(coro) == 'suspended' then
+ running = true
+ -- directly call coroutine.resume() instead of our resume()
+ -- wrapper because we explicitly check for errors here
+ local ok, message = coroutine.resume(coro)
+ if not ok then
+ print('*** ' .. message)
+ errors += 1
+ end
+ if coroutine.status(coro) == 'dead' then
+ coros[i] = nil
+ end
+ end
+ end
+ until not running
+ return errors
+end
+
+joinall(coros)
+
+print(string.format('%q', table.concat(result, ' ')))
+assert(util.equal(values, result))
+
+-- ----------------------------- test ErrorQueue -----------------------------
+q4 = ErrorQueue()
+result = {}
+values = { 'This', 'is', 'a', 'test', 'script' }
+
+coros = {}
+for _, name in {'a', 'b'} do
+ local coro = coroutine.create(consumer)
+ table.insert(coros, coro)
+ -- Resuming both coroutines should leave them both waiting for a queue item.
+ resume(coro, name, q4)
+end
+
+for i = 1, 4 do
+ print(string.format('Enqueue(%q)', values[i]))
+ q4:Enqueue(values[i])
+end
+q4:Error('something went wrong')
+
+assert(joinall(coros) == 2)
+
diff --git a/indra/newview/scripts/lua/require/ErrorQueue.lua b/indra/newview/scripts/lua/require/ErrorQueue.lua
new file mode 100644
index 0000000000..e6e9a5ef48
--- /dev/null
+++ b/indra/newview/scripts/lua/require/ErrorQueue.lua
@@ -0,0 +1,37 @@
+-- ErrorQueue isa WaitQueue with the added feature that a producer can push an
+-- error through the queue. Once that error is dequeued, every consumer will
+-- raise that error.
+
+local WaitQueue = require('WaitQueue')
+local function dbg(...) end
+-- local dbg = require('printf')
+local util = require('util')
+
+local ErrorQueue = WaitQueue()
+
+util.classctor(ErrorQueue)
+
+function ErrorQueue:Error(message)
+ -- Setting Error() is a marker, like closing the queue. Once we reach the
+ -- error, every subsequent Dequeue() call will raise the same error.
+ dbg('Setting self._closed to %q', message)
+ self._closed = message
+ self:_wake_waiters()
+end
+
+function ErrorQueue:Dequeue()
+ local value = WaitQueue.Dequeue(self)
+ dbg('ErrorQueue:Dequeue: base Dequeue() got %s', value)
+ if value ~= nil then
+ -- queue not yet closed, show caller
+ return value
+ end
+ if self._closed == true then
+ -- WaitQueue:close() sets true: queue has only been closed, tell caller
+ return nil
+ end
+ -- self._closed is a message set by Error()
+ error(self._closed)
+end
+
+return ErrorQueue
diff --git a/indra/newview/scripts/lua/require/LLAgent.lua b/indra/newview/scripts/lua/require/LLAgent.lua
new file mode 100644
index 0000000000..4a1132fe7e
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLAgent.lua
@@ -0,0 +1,143 @@
+local leap = require 'leap'
+local mapargs = require 'mapargs'
+
+local LLAgent = {}
+
+function LLAgent.getRegionPosition()
+ return leap.request('LLAgent', {op = 'getPosition'}).region
+end
+
+function LLAgent.getGlobalPosition()
+ return leap.request('LLAgent', {op = 'getPosition'}).global
+end
+
+-- Return array information about the agent's groups
+-- id: group id\n"
+-- name: group name\n"
+-- insignia: group insignia texture id
+-- notices: bool indicating if this user accepts notices from this group
+-- display: bool indicating if this group is listed in the user's profile
+-- contrib: user's land contribution to this group
+function LLAgent.getGroups()
+ return leap.request('LLAgent', {op = 'getGroups'}).groups
+end
+
+-- Use LL.leaphelp('LLAgent') and see 'setCameraParams' to get more info about params
+-- -- TYPE -- DEFAULT -- RANGE
+-- LLAgent.setCamera{ [, camera_pos] -- vector3
+-- [, focus_pos] -- vector3
+-- [, focus_offset] -- vector3 -- {1,0,0} -- {-10,-10,-10} to {10,10,10}
+-- [, distance] -- float (meters) -- 3 -- 0.5 to 50
+-- [, focus_threshold] -- float (meters) -- 1 -- 0 to 4
+-- [, camera_threshold] -- float (meters) -- 1 -- 0 to 4
+-- [, focus_lag] -- float (seconds) -- 0.1 -- 0 to 3
+-- [, camera_lag] -- float (seconds) -- 0.1 -- 0 to 3
+-- [, camera_pitch] -- float (degrees) -- 0 -- -45 to 80
+-- [, behindness_angle] -- float (degrees) -- 10 -- 0 to 180
+-- [, behindness_lag] -- float (seconds) -- 0 -- 0 to 3
+-- [, camera_locked] -- bool -- false
+-- [, focus_locked]} -- bool -- false
+function LLAgent.setCamera(...)
+ local args = mapargs('camera_pos,focus_pos,focus_offset,focus_lag,camera_lag,' ..
+ 'distance,focus_threshold,camera_threshold,camera_pitch,' ..
+ 'camera_locked,focus_locked,behindness_angle,behindness_lag', ...)
+ args.op = 'setCameraParams'
+ leap.send('LLAgent', args)
+end
+
+function LLAgent.setFollowCamActive(active)
+ leap.send('LLAgent', {op = 'setFollowCamActive', active = active})
+end
+
+function LLAgent.removeCamParams()
+ leap.send('LLAgent', {op = 'removeCameraParams'})
+end
+
+-- Play specified animation by "item_id" locally
+-- if "inworld" is specified as true, animation will be played inworld instead
+function LLAgent.playAnimation(...)
+ local args = mapargs('item_id,inworld', ...)
+ args.op = 'playAnimation'
+ return leap.request('LLAgent', args)
+end
+
+function LLAgent.stopAnimation(item_id)
+ return leap.request('LLAgent', {op = 'stopAnimation', item_id=item_id})
+end
+
+-- Get animation info by "item_id"
+-- reply contains "duration", "is_loop", "num_joints", "asset_id", "priority"
+function LLAgent.getAnimationInfo(item_id)
+ return leap.request('LLAgent', {op = 'getAnimationInfo', item_id=item_id}).anim_info
+end
+
+-- Teleport to specified "regionname" at specified region-relative "x", "y", "z".
+-- If "regionname" is "home", ignore "x", "y", "z" and teleport home.
+-- If "regionname" omitted, teleport to GLOBAL coordinates "x", "y", "z".
+function LLAgent.teleport(...)
+ local args = mapargs('regionname,x,y,z', ...)
+ args.op = 'teleport'
+ return leap.request('LLTeleportHandler', args).message
+end
+
+-- Call with no arguments to sit on the ground.
+-- Otherwise specify "obj_uuid" to sit on,
+-- or region "position" {x, y, z} where to find closest object to sit on.
+-- For example: LLAgent.requestSit{position=LLAgent.getRegionPosition()}
+-- Your avatar should be close enough to the object you want to sit on
+function LLAgent.requestSit(...)
+ local args = mapargs('obj_uuid,position', ...)
+ args.op = 'requestSit'
+ return leap.request('LLAgent', args)
+end
+
+function LLAgent.requestStand()
+ leap.send('LLAgent', {op = 'requestStand'})
+end
+
+-- ***************************************************************************
+-- Autopilot
+-- ***************************************************************************
+LLAgent.autoPilotPump = "LLAutopilot"
+
+-- Start the autopilot to move to "target_global" location using specified parameters
+-- LLAgent.startAutoPilot{ target_global array of target global {x, y, z} position
+-- [, allow_flying] allow flying during autopilot [default: true]
+-- [, stop_distance] target maximum distance from target [default: autopilot guess]
+-- [, behavior_name] name of the autopilot behavior [default: (script name)]
+-- [, target_rotation] array of [x, y, z, w] quaternion values [default: no target]
+-- [, rotation_threshold] target maximum angle from target facing rotation [default: 0.03 radians]
+-- an event with "success" flag is sent to "LLAutopilot" event pump, when auto pilot is terminated
+function LLAgent.startAutoPilot(...)
+ local args = mapargs('target_global,allow_flying,stop_distance,behavior_name,target_rotation,rotation_threshold', ...)
+ args.op = 'startAutoPilot'
+ leap.send('LLAgent', args)
+end
+
+-- Update target location for currently running autopilot
+function LLAgent.setAutoPilotTarget(target_global)
+ leap.send('LLAgent', {op = 'setAutoPilotTarget', target_global=target_global})
+end
+
+-- Start the autopilot to move to the specified target location
+-- either "leader_id" (uuid of target) or "avatar_name" (avatar full name: use just first name for 'Resident') should be specified
+-- "allow_flying" [default: true], "stop_distance" [default: autopilot guess]
+function LLAgent.startFollowPilot(...)
+ local args = mapargs('leader_id,avatar_name,allow_flying,stop_distance', ...)
+ args.op = 'startFollowPilot'
+ return leap.request('LLAgent', args)
+end
+
+-- Stop the autopilot system: "user_cancel" indicates whether or not to act as though user canceled autopilot [default: false]
+function LLAgent.stopAutoPilot(...)
+ local args = mapargs('user_cancel', ...)
+ args.op = 'stopAutoPilot'
+ leap.send('LLAgent', args)
+end
+
+-- Get information about current state of the autopilot
+function LLAgent.getAutoPilot()
+ return leap.request('LLAgent', {op = 'getAutoPilot'})
+end
+
+return LLAgent
diff --git a/indra/newview/scripts/lua/require/LLAppearance.lua b/indra/newview/scripts/lua/require/LLAppearance.lua
new file mode 100644
index 0000000000..f533d22daf
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLAppearance.lua
@@ -0,0 +1,31 @@
+local leap = require 'leap'
+
+local LLAppearance = {}
+
+function LLAppearance.wearOutfit(folder, action)
+ action = action or 'add'
+ leap.request('LLAppearance', {op='wearOutfit', append = (action == 'add'), folder_id=folder})
+end
+
+function LLAppearance.wearOutfitByName(folder, action)
+ action = action or 'add'
+ leap.request('LLAppearance', {op='wearOutfit', append = (action == 'add'), folder_name=folder})
+end
+
+function LLAppearance.wearItems(items_id, replace)
+ leap.send('LLAppearance', {op='wearItems', replace = replace, items_id=items_id})
+end
+
+function LLAppearance.detachItems(items_id)
+ leap.send('LLAppearance', {op='detachItems', items_id=items_id})
+end
+
+function LLAppearance.getOutfitsList()
+ return leap.request('LLAppearance', {op='getOutfitsList'})['outfits']
+end
+
+function LLAppearance.getOutfitItems(id)
+ return leap.request('LLAppearance', {op='getOutfitItems', outfit_id = id})['items']
+end
+
+return LLAppearance
diff --git a/indra/newview/scripts/lua/require/LLChat.lua b/indra/newview/scripts/lua/require/LLChat.lua
new file mode 100644
index 0000000000..3ac3bab746
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLChat.lua
@@ -0,0 +1,39 @@
+local leap = require 'leap'
+
+local LLChat = {}
+
+-- ***************************************************************************
+-- Nearby chat
+-- ***************************************************************************
+LLChat.nearbyChatPump = "LLNearbyChat"
+
+-- 0 is public nearby channel, other channels are used to communicate with LSL scripts
+function LLChat.sendNearby(msg, channel)
+ leap.send('LLChatBar', {op='sendChat', message=msg, channel=channel})
+end
+
+function LLChat.sendWhisper(msg)
+ leap.send('LLChatBar', {op='sendChat', type='whisper', message=msg})
+end
+
+function LLChat.sendShout(msg)
+ leap.send('LLChatBar', {op='sendChat', type='shout', message=msg})
+end
+
+-- ***************************************************************************
+-- Group chat
+-- ***************************************************************************
+
+function LLChat.startGroupChat(group_id)
+ return leap.request('GroupChat', {op='startGroupChat', group_id=group_id})
+end
+
+function LLChat.leaveGroupChat(group_id)
+ leap.send('GroupChat', {op='leaveGroupChat', group_id=group_id})
+end
+
+function LLChat.sendGroupIM(msg, group_id)
+ leap.send('GroupChat', {op='sendGroupIM', message=msg, group_id=group_id})
+end
+
+return LLChat
diff --git a/indra/newview/scripts/lua/require/LLDebugSettings.lua b/indra/newview/scripts/lua/require/LLDebugSettings.lua
new file mode 100644
index 0000000000..cff1a63c21
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLDebugSettings.lua
@@ -0,0 +1,17 @@
+local leap = require 'leap'
+
+local LLDebugSettings = {}
+
+function LLDebugSettings.set(name, value)
+ leap.request('LLViewerControl', {op='set', group='Global', key=name, value=value})
+end
+
+function LLDebugSettings.toggle(name)
+ leap.request('LLViewerControl', {op='toggle', group='Global', key=name})
+end
+
+function LLDebugSettings.get(name)
+ return leap.request('LLViewerControl', {op='get', group='Global', key=name})['value']
+end
+
+return LLDebugSettings
diff --git a/indra/newview/scripts/lua/require/LLFloaterAbout.lua b/indra/newview/scripts/lua/require/LLFloaterAbout.lua
new file mode 100644
index 0000000000..a6e42d364f
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLFloaterAbout.lua
@@ -0,0 +1,11 @@
+-- Engage the LLFloaterAbout LLEventAPI
+
+local leap = require 'leap'
+
+local LLFloaterAbout = {}
+
+function LLFloaterAbout.getInfo()
+ return leap.request('LLFloaterAbout', {op='getInfo'})
+end
+
+return LLFloaterAbout
diff --git a/indra/newview/scripts/lua/require/LLGesture.lua b/indra/newview/scripts/lua/require/LLGesture.lua
new file mode 100644
index 0000000000..343b611e2c
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLGesture.lua
@@ -0,0 +1,23 @@
+-- Engage the LLGesture LLEventAPI
+
+local leap = require 'leap'
+
+local LLGesture = {}
+
+function LLGesture.getActiveGestures()
+ return leap.request('LLGesture', {op='getActiveGestures'})['gestures']
+end
+
+function LLGesture.isGesturePlaying(id)
+ return leap.request('LLGesture', {op='isGesturePlaying', id=id})['playing']
+end
+
+function LLGesture.startGesture(id)
+ leap.send('LLGesture', {op='startGesture', id=id})
+end
+
+function LLGesture.stopGesture(id)
+ leap.send('LLGesture', {op='stopGesture', id=id})
+end
+
+return LLGesture
diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua
new file mode 100644
index 0000000000..2c80a8602b
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLInventory.lua
@@ -0,0 +1,67 @@
+local leap = require 'leap'
+local mapargs = require 'mapargs'
+local result_view = require 'result_view'
+
+local function result(keys)
+ -- capture result_view() instances for both categories and items
+ local result_table = {
+ categories=result_view(keys.categories),
+ items=result_view(keys.items),
+ -- call result_table:close() to release result sets before garbage
+ -- collection or script completion
+ close = function(self)
+ result_view.close(keys.categories[1], keys.items[1])
+ end
+ }
+ -- When the result_table is destroyed, close its result_views.
+ return LL.setdtor('LLInventory result', result_table, result_table.close)
+end
+
+local LLInventory = {}
+
+-- Get the items/folders info by provided IDs,
+-- reply will contain "items" and "categories" tables accordingly
+function LLInventory.getItemsInfo(item_ids)
+ return result(leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids}))
+end
+
+-- Get the table of folder type names, which can be later used to get the ID of the basic folders
+function LLInventory.getFolderTypeNames()
+ return leap.request('LLInventory', {op = 'getFolderTypeNames'}).names
+end
+
+-- Get the UUID of the basic folder("Textures", "My outfits", "Sounds" etc.) by specified folder type name
+function LLInventory.getBasicFolderID(ft_name)
+ return leap.request('LLInventory', {op = 'getBasicFolderID', ft_name=ft_name}).id
+end
+
+-- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendantsIf(...)
+function LLInventory.getAssetTypeNames()
+ return leap.request('LLInventory', {op = 'getAssetTypeNames'}).names
+end
+
+-- Get the direct descendants of the 'folder_id' provided,
+-- reply will contain "items" and "categories" tables accordingly
+function LLInventory.getDirectDescendants(folder_id)
+ return result(leap.request('LLInventory', {op = 'getDirectDescendants', folder_id=folder_id}))
+end
+-- backwards compatibility
+LLInventory.getDirectDescendents = LLInventory.getDirectDescendants
+
+-- Get the descendants of the 'folder_id' provided, which pass specified filters
+-- reply will contain "items" and "categories" tables accordingly
+-- LLInventory.collectDescendantsIf{ folder_id -- parent folder ID
+-- [, name] -- name (substring)
+-- [, desc] -- description (substring)
+-- [, type] -- asset type
+-- [, limit] -- item count limit in reply, maximum and default is 100
+-- [, filter_links]} -- EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)
+function LLInventory.collectDescendantsIf(...)
+ local args = mapargs('folder_id,name,desc,type,filter_links,limit', ...)
+ args.op = 'collectDescendantsIf'
+ return result(leap.request('LLInventory', args))
+end
+-- backwards compatibility
+LLInventory.collectDescendentsIf = LLInventory.collectDescendantsIf
+
+return LLInventory
diff --git a/indra/newview/scripts/lua/require/LLListener.lua b/indra/newview/scripts/lua/require/LLListener.lua
new file mode 100644
index 0000000000..b05f966097
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLListener.lua
@@ -0,0 +1,50 @@
+local fiber = require 'fiber'
+local inspect = require 'inspect'
+local leap = require 'leap'
+local util = require 'util'
+
+local LLListener = {}
+local waitfor = {}
+local listener_name = {}
+
+function LLListener:new(pump_name)
+ local obj = setmetatable({}, self)
+ self.__index = self
+ obj.name = 'Listener:' .. pump_name
+ obj._pump = pump_name
+
+ return obj
+end
+
+util.classctor(LLListener)
+
+function LLListener:handleMessages(event_data)
+ print(inspect(event_data))
+ return true
+end
+
+function LLListener:start()
+ _pump = self._pump
+ waitfor = leap.WaitFor(-1, self.name)
+ function waitfor:filter(pump, data)
+ if _pump == pump then
+ return data
+ end
+ end
+
+ fiber.launch(self.name, function()
+ event = waitfor:wait()
+ while event and self:handleMessages(event) do
+ event = waitfor:wait()
+ end
+ end)
+
+ listener_name = leap.request(leap.cmdpump(), {op='listen', source=_pump, listener="LLListener", tweak=true}).listener
+end
+
+function LLListener:stop()
+ leap.send(leap.cmdpump(), {op='stoplistening', source=self._pump, listener=listener_name})
+ waitfor:close()
+end
+
+return LLListener
diff --git a/indra/newview/scripts/lua/require/Queue.lua b/indra/newview/scripts/lua/require/Queue.lua
new file mode 100644
index 0000000000..5bc72e4057
--- /dev/null
+++ b/indra/newview/scripts/lua/require/Queue.lua
@@ -0,0 +1,51 @@
+-- from https://create.roblox.com/docs/luau/queues#implementing-queues,
+-- amended per https://www.lua.org/pil/16.1.html
+
+-- While coding some scripting in Lua
+-- I found that I needed a queua
+-- I thought of linked list
+-- But had to resist
+-- For fear it might be too obscua.
+
+local util = require 'util'
+
+local Queue = {}
+
+function Queue:new()
+ local obj = setmetatable({}, self)
+ self.__index = self
+
+ obj._first = 0
+ obj._last = -1
+ obj._queue = {}
+
+ return obj
+end
+
+util.classctor(Queue)
+
+-- Check if the queue is empty
+function Queue:IsEmpty()
+ return self._first > self._last
+end
+
+-- Add a value to the queue
+function Queue:Enqueue(value)
+ local last = self._last + 1
+ self._last = last
+ self._queue[last] = value
+end
+
+-- Remove a value from the queue
+function Queue:Dequeue()
+ if self:IsEmpty() then
+ return nil
+ end
+ local first = self._first
+ local value = self._queue[first]
+ self._queue[first] = nil
+ self._first = first + 1
+ return value
+end
+
+return Queue
diff --git a/indra/newview/scripts/lua/require/Region.lua b/indra/newview/scripts/lua/require/Region.lua
new file mode 100644
index 0000000000..e4eefece33
--- /dev/null
+++ b/indra/newview/scripts/lua/require/Region.lua
@@ -0,0 +1,17 @@
+LLFloaterAbout = require 'LLFloaterAbout'
+
+local Region = {}
+
+function Region.getInfo()
+ info = LLFloaterAbout.getInfo()
+ return {
+ HOSTNAME=info.HOSTNAME,
+ POSITION=info.POSITION,
+ POSITION_LOCAL=info.POSITION_LOCAL,
+ REGION=info.REGION,
+ SERVER_VERSION=info.SERVER_VERSION,
+ SLURL=info.SLURL,
+ }
+end
+
+return Region
diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua
new file mode 100644
index 0000000000..cf2695917e
--- /dev/null
+++ b/indra/newview/scripts/lua/require/UI.lua
@@ -0,0 +1,243 @@
+-- Engage the viewer's UI
+
+local leap = require 'leap'
+local mapargs = require 'mapargs'
+local result_view = require 'result_view'
+local Timer = (require 'timers').Timer
+local util = require 'util'
+
+-- Allow lazily accessing UI submodules on demand, e.g. a reference to
+-- UI.Floater lazily loads the UI/Floater module.
+local UI = util.submoduledir({}, '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.callables()
+ return leap.request('UI', {op='callables'}).callables
+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
+
+-- ***************************************************************************
+-- Snapshot
+-- ***************************************************************************
+-- UI.snapshot{filename=filename -- extension may be specified: bmp, jpeg, png
+-- [, type='COLOR' | 'DEPTH']
+-- [, width=width][, height=height] -- uses current window size if not specified
+-- [, showui=true][, showhud=true]
+-- [, rebuild=false]}
+function UI.snapshot(...)
+ local args = mapargs('filename,width,height,showui,showhud,rebuild,type', ...)
+ args.op = 'saveSnapshot'
+ return leap.request('LLViewerWindow', args).result
+end
+
+-- ***************************************************************************
+-- Top menu
+-- ***************************************************************************
+
+function UI.getTopMenus()
+ return leap.request('UI', {op='getTopMenus'}).menus
+end
+
+function UI.addMenu(...)
+ local args = mapargs('name,label', ...)
+ args.op = 'addMenu'
+ return leap.request('UI', args)
+end
+
+function UI.setMenuVisible(name, visible)
+ return leap.request('UI', {op='setMenuVisible', name=name, visible=visible})
+end
+
+function UI.addMenuBranch(...)
+ local args = mapargs('name,label,parent_menu', ...)
+ args.op = 'addMenuBranch'
+ return leap.request('UI', args)
+end
+
+-- see UI.callables() for valid values of 'func'
+function UI.addMenuItem(...)
+ local args = mapargs('name,label,parent_menu,func,param,pos', ...)
+ args.op = 'addMenuItem'
+ return leap.request('UI', args)
+end
+
+function UI.addMenuSeparator(...)
+ local args = mapargs('parent_menu,pos', ...)
+ args.op = 'addMenuSeparator'
+ return leap.request('UI', args)
+end
+
+-- ***************************************************************************
+-- Toolbar buttons
+-- ***************************************************************************
+-- Clears all buttons off the toolbars
+function UI.clearAllToolbars()
+ leap.send('UI', {op='clearAllToolbars'})
+end
+
+function UI.defaultToolbars()
+ leap.send('UI', {op='defaultToolbars'})
+end
+
+-- UI.addToolbarBtn{btn_name=btn_name
+-- [, toolbar= bottom] -- left, right, bottom -- default is bottom
+-- [, rank=1]} -- position on the toolbar, starts at 0 (0 - first position, 1 - second position etc.)
+function UI.addToolbarBtn(...)
+ local args = mapargs('btn_name,toolbar,rank', ...)
+ args.op = 'addToolbarBtn'
+ return leap.request('UI', args)
+end
+
+-- Returns the rank(position) of the command in the original list
+function UI.removeToolbarBtn(btn_name)
+ return leap.request('UI', {op = 'removeToolbarBtn', btn_name=btn_name}).rank
+end
+
+function UI.getToolbarBtnNames()
+ return leap.request('UI', {op = 'getToolbarBtnNames'}).cmd_names
+end
+
+-- ***************************************************************************
+-- Floaters
+-- ***************************************************************************
+function UI.showFloater(floater_name)
+ leap.send("LLFloaterReg", {op = "showInstance", name = floater_name})
+end
+
+function UI.hideFloater(floater_name)
+ leap.send("LLFloaterReg", {op = "hideInstance", name = floater_name})
+end
+
+function UI.toggleFloater(floater_name)
+ leap.send("LLFloaterReg", {op = "toggleInstance", name = floater_name})
+end
+
+function UI.isFloaterVisible(floater_name)
+ return leap.request("LLFloaterReg", {op = "instanceVisible", name = floater_name}).visible
+end
+
+function UI.closeAllFloaters()
+ return leap.send("UI", {op = "closeAllFloaters"})
+end
+
+function UI.getFloaterNames()
+ local key_length = leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters
+ local view = result_view(key_length)
+ return LL.setdtor('registered floater names', view, view.close)
+end
+
+return UI
diff --git a/indra/newview/scripts/lua/require/UI/Floater.lua b/indra/newview/scripts/lua/require/UI/Floater.lua
new file mode 100644
index 0000000000..d057a74386
--- /dev/null
+++ b/indra/newview/scripts/lua/require/UI/Floater.lua
@@ -0,0 +1,146 @@
+-- Floater base class
+
+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
+local event_set = {}
+for _, event in pairs(event_list) do
+ event_set[event] = true
+end
+
+local function _event(event_name)
+ if not event_set[event_name] then
+ error("Incorrect event name: " .. event_name, 3)
+ end
+ return event_name
+end
+
+-- ---------------------------------------------------------------------------
+local Floater = {}
+
+-- Pass:
+-- relative file path to floater's XUI definition file
+-- optional: sign up for additional events for defined control
+-- {<control_name>={action1, action2, ...}}
+function Floater:new(path, extra)
+ local obj = setmetatable({}, self)
+ self.__index = self
+
+ local path_parts = string.split(path, '/')
+ obj.name = 'Floater ' .. path_parts[#path_parts]
+
+ obj._command = {op="showLuaFloater", xml_path=LL.abspath(path)}
+ if extra then
+ -- validate each of the actions for each specified control
+ for control, actions in pairs(extra) do
+ for _, action in pairs(actions) do
+ _event(action)
+ end
+ end
+ obj._command.extra_events = extra
+ end
+
+ 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
+ -- 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 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
+ -- handleEvents() returns false, we're done.
+ if not self:handleEvents(event) then
+ return
+ end
+end
+
+function Floater:post(action)
+ leap.send(self._pump, action)
+end
+
+function Floater:request(action)
+ return leap.request(self._pump, action)
+end
+
+-- local inspect = require 'inspect'
+
+function Floater:handleEvents(event_data)
+ local event = event_data.event
+ if event_set[event] == nil then
+ LL.print_warning(string.format('%s received unknown event %q', self.name, event))
+ end
+
+ -- Before checking for a general (e.g.) commit() method, first look for
+ -- commit_ctrl_name(): in other words, concatenate the event name with the
+ -- ctrl_name, with an underscore between. If there exists such a specific
+ -- method, call that.
+ local handler, ret
+ if event_data.ctrl_name then
+ local specific = event .. '_' .. event_data.ctrl_name
+ handler = self[specific]
+ if handler then
+ ret = handler(self, event_data)
+ -- Avoid 'return ret or true' because we explicitly want to allow
+ -- the handler to return false.
+ if ret ~= nil then
+ return ret
+ else
+ return true
+ end
+ end
+ end
+
+ -- No specific "event_on_ctrl()" method found; try just "event()"
+ handler = self[event]
+ if handler then
+ ret = handler(self, event_data)
+ if ret ~= nil then
+ return ret
+ end
+-- else
+-- print(string.format('%s ignoring event %s', self.name, inspect(event_data)))
+ end
+
+ -- 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 leap.eventstream().
+ if event == _event('floater_close') then
+ LL.print_warning(self.name .. ' closed')
+ return false
+ end
+ return true
+end
+
+-- onCtrl() permits a different dispatch style in which the general event()
+-- method explicitly calls (e.g.)
+-- self:onCtrl(event_data, {
+-- ctrl_name=function()
+-- self:post(...)
+-- end,
+-- ...
+-- })
+function Floater:onCtrl(event_data, ctrl_map)
+ local handler = ctrl_map[event_data.ctrl_name]
+ if handler then
+ handler()
+ end
+end
+
+return Floater
diff --git a/indra/newview/scripts/lua/require/UI/popup.lua b/indra/newview/scripts/lua/require/UI/popup.lua
new file mode 100644
index 0000000000..8ccf3b87f3
--- /dev/null
+++ b/indra/newview/scripts/lua/require/UI/popup.lua
@@ -0,0 +1,82 @@
+local leap = require 'leap'
+local mapargs = require 'mapargs'
+local util = require 'util'
+
+-- 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, returning nil
+-- wait=true waits for user response:
+-- * If the viewer returns a table containing exactly one key=true pair,
+-- popup() returns just that key. If the key is a string containing an
+-- underscore, e.g. 'OK_okcancelbuttons', it's truncated at the first
+-- underscore, e.g. 'OK'.
+-- * Otherwise the viewer's response is returned unchanged. To suppress the
+-- above transformations, pass a non-empty payload table; this will cause
+-- the viewer to return a table with at least two keys.
+local popup = util.setmetamethods{
+ -- 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
+ newargs = {op='requestAdd',
+ name=args.notification,
+ substitutions=args.vars,
+ payload=args.payload}
+ -- Specifically test (wait == false), NOT (not wait), because we treat
+ -- nil (omitted, default true) differently than false (explicitly
+ -- DON'T wait).
+ if args.wait == false then
+ leap.send('LLNotifications', newargs)
+ else
+ local response = leap.request('LLNotifications', newargs).response
+ -- response is typically a table. It might have multiple keys,
+ -- e.g. if caller passed non-empty payload. In that case, just
+ -- return the whole thing.
+ if type(response) ~= 'table' then
+ return response
+ end
+ -- get first key=value pair, if any
+ local key, value = next(response)
+ if (not key) or next(response, key) then
+ -- key == nil means response is empty
+ -- next(response, non-nil first key) ~= nil means at least two keys
+ return response
+ end
+ -- Here response is a table containing exactly one key. The
+ -- notifications system typically returns a table of the form
+ -- {OK_okcancelbuttons=true}, which is tricky to test for because it
+ -- varies with each set of buttons.
+ if value == true then
+ -- change {key=true} to plain key
+ response = key
+ if type(response) == 'string' then
+ -- change 'OK_okcancelbuttons' to plain 'OK'
+ response = string.split(response, '_')[1]
+ end
+ end
+ return response
+ end
+ end
+}
+
+function popup:alert(message, payload)
+ return self('GenericAlert', {MESSAGE=message, payload=payload})
+end
+
+function popup:alertOK(message, payload)
+ return self('GenericAlertOK', {MESSAGE=message, payload=payload})
+end
+
+function popup:alertYesCancel(message, payload)
+ return self('GenericAlertYesCancel', {MESSAGE=message, payload=payload})
+end
+
+function popup:tip(message, payload)
+ self{'SystemMessageTip', {MESSAGE=message, payload=payload}, wait=false}
+end
+
+return popup
diff --git a/indra/newview/scripts/lua/require/WaitQueue.lua b/indra/newview/scripts/lua/require/WaitQueue.lua
new file mode 100644
index 0000000000..7e10d03295
--- /dev/null
+++ b/indra/newview/scripts/lua/require/WaitQueue.lua
@@ -0,0 +1,88 @@
+-- WaitQueue isa Queue with the added feature that when the queue is empty,
+-- the Dequeue() operation blocks the calling coroutine until some other
+-- coroutine Enqueue()s a new value.
+
+local fiber = require('fiber')
+local Queue = require('Queue')
+local util = require('util')
+
+local function dbg(...) end
+-- local dbg = require('printf')
+
+local WaitQueue = Queue()
+
+function WaitQueue:new()
+ local obj = Queue()
+ setmetatable(obj, self)
+ self.__index = self
+
+ obj._waiters = {}
+ obj._closed = false
+ return obj
+end
+
+util.classctor(WaitQueue)
+
+function WaitQueue:Enqueue(value)
+ if self._closed then
+ error("can't Enqueue() on closed Queue")
+ end
+ -- can't simply call Queue:Enqueue(value)! That calls the method on the
+ -- Queue class definition, instead of calling Queue:Enqueue() on self.
+ -- Hand-expand the Queue:Enqueue() syntactic sugar.
+ Queue.Enqueue(self, value)
+ self:_wake_waiters()
+end
+
+function WaitQueue:_wake_waiters()
+ -- WaitQueue is designed to support multi-producer, multi-consumer use
+ -- cases. With multiple consumers, if more than one is trying to
+ -- Dequeue() from an empty WaitQueue, we'll have multiple waiters.
+ -- Unlike OS threads, with cooperative concurrency it doesn't make sense
+ -- to "notify all": we need wake only one of the waiting Dequeue()
+ -- callers.
+ if ((not self:IsEmpty()) or self._closed) and next(self._waiters) then
+ -- Pop the oldest waiting coroutine instead of the most recent, for
+ -- more-or-less round robin fairness. But skip any coroutines that
+ -- have gone dead in the meantime.
+ local waiter = table.remove(self._waiters, 1)
+ while waiter and fiber.status(waiter) == "dead" do
+ waiter = table.remove(self._waiters, 1)
+ end
+ -- do we still have at least one waiting coroutine?
+ if waiter then
+ -- don't pass the head item: let the resumed coroutine retrieve it
+ fiber.wake(waiter)
+ end
+ end
+end
+
+function WaitQueue:Dequeue()
+ while self:IsEmpty() do
+ -- Don't check for closed until the queue is empty: producer can close
+ -- the queue while there are still items left, and we want the
+ -- consumer(s) to retrieve those last few items.
+ if self._closed then
+ dbg('WaitQueue:Dequeue(): closed')
+ return nil
+ end
+ dbg('WaitQueue:Dequeue(): waiting')
+ -- add the running coroutine to the list of waiters
+ dbg('WaitQueue:Dequeue() running %s', tostring(coroutine.running() or 'main'))
+ table.insert(self._waiters, fiber.running())
+ -- then let somebody else run
+ fiber.wait()
+ end
+ -- here we're sure this queue isn't empty
+ dbg('WaitQueue:Dequeue() calling Queue.Dequeue()')
+ return Queue.Dequeue(self)
+end
+
+function WaitQueue:close()
+ self._closed = true
+ -- close() is like Enqueueing an end marker. If there are waiting
+ -- consumers, give them a chance to see we're closed.
+ self:_wake_waiters()
+end
+
+return WaitQueue
diff --git a/indra/newview/scripts/lua/require/coro.lua b/indra/newview/scripts/lua/require/coro.lua
new file mode 100644
index 0000000000..616a797e95
--- /dev/null
+++ b/indra/newview/scripts/lua/require/coro.lua
@@ -0,0 +1,67 @@
+-- Manage Lua coroutines
+
+local coro = {}
+
+coro._coros = {}
+
+-- Launch a Lua coroutine: create and resume.
+-- Returns: new coroutine, values yielded or returned from initial resume()
+-- If initial resume() encountered an error, propagates the error.
+function coro.launch(func, ...)
+ local co = coroutine.create(func)
+ table.insert(coro._coros, co)
+ return co, coro.resume(co, ...)
+end
+
+-- resume() wrapper to propagate errors
+function coro.resume(co, ...)
+ -- if there's an idiom other than table.pack() to assign an arbitrary
+ -- number of return values, I don't yet know it
+ local ok_result = table.pack(coroutine.resume(co, ...))
+ if not ok_result[1] then
+ -- if [1] is false, then [2] is the error message
+ error(ok_result[2])
+ end
+ -- ok is true, whew, just return the rest of the values
+ return table.unpack(ok_result, 2)
+end
+
+-- yield to other coroutines even if you don't know whether you're in a
+-- created coroutine or the main coroutine
+function coro.yield(...)
+ if coroutine.running() then
+ -- this is a real coroutine, yield normally
+ return coroutine.yield(...)
+ else
+ -- This is the main coroutine: coroutine.yield() doesn't work.
+ -- But we can take a spin through previously-launched coroutines.
+ -- Walk a copy of coro._coros in case any of these coroutines launches
+ -- another: next() forbids creating new entries during traversal.
+ for co in coro._live_coros_iter, table.clone(coro._coros) do
+ coro.resume(co)
+ end
+ end
+end
+
+-- Walk coro._coros table, returning running or suspended coroutines.
+-- Once a coroutine becomes dead, remove it from _coros and don't return it.
+function coro._live_coros()
+ return coro._live_coros_iter, coro._coros
+end
+
+-- iterator function for _live_coros()
+function coro._live_coros_iter(t, idx)
+ local k, co = next(t, idx)
+ while k and coroutine.status(co) == 'dead' do
+-- t[k] = nil
+ -- See coro.yield(): sometimes we traverse a copy of _coros, but if we
+ -- discover a dead coroutine in that copy, delete it from _coros
+ -- anyway. Deleting it from a temporary copy does nothing.
+ coro._coros[k] = nil
+ coroutine.close(co)
+ k, co = next(t, k)
+ end
+ return co
+end
+
+return coro
diff --git a/indra/newview/scripts/lua/require/fiber.lua b/indra/newview/scripts/lua/require/fiber.lua
new file mode 100644
index 0000000000..b3c684dd67
--- /dev/null
+++ b/indra/newview/scripts/lua/require/fiber.lua
@@ -0,0 +1,346 @@
+-- Organize Lua coroutines into fibers.
+
+-- In this usage, the difference between coroutines and fibers is that fibers
+-- have a scheduler. Yielding a fiber means allowing other fibers, plural, to
+-- run: it's more than just returning control to the specific Lua thread that
+-- resumed the running coroutine.
+
+-- fiber.launch() creates a new fiber ready to run.
+-- fiber.status() reports (augmented) status of the passed fiber: instead of
+-- 'suspended', it returns either 'ready' or 'waiting'
+-- fiber.yield() allows other fibers to run, but leaves the calling fiber
+-- ready to run.
+-- fiber.wait() marks the running fiber not ready, and resumes other fibers.
+-- fiber.wake() marks the designated suspended fiber ready to run, but does
+-- not yet resume it.
+-- fiber.run() runs all current fibers until all have terminated (successfully
+-- or with an error).
+
+local printf = require 'printf'
+local function dbg(...) end
+-- local dbg = printf
+local coro = require 'coro'
+
+local fiber = {}
+
+-- The tables in which we track fibers must have weak keys so dead fibers
+-- can be garbage-collected.
+local weak_values = {__mode='v'}
+local weak_keys = {__mode='k'}
+
+-- Track each current fiber as being either ready to run or not ready
+-- (waiting). wait() moves the running fiber from ready to waiting; wake()
+-- moves the designated fiber from waiting back to ready.
+-- The ready table is used as a list so yield() can go round robin.
+local ready = setmetatable({'main'}, weak_keys)
+-- The waiting table is used as a set because order doesn't matter.
+local waiting = setmetatable({}, weak_keys)
+
+-- Every fiber has a name, for diagnostic purposes. Names must be unique.
+-- A colliding name will be suffixed with an integer.
+-- Predefine 'main' with our marker so nobody else claims that name.
+local names = setmetatable({main='main'}, weak_keys)
+local byname = setmetatable({main='main'}, weak_values)
+-- each colliding name has its own distinct suffix counter
+local suffix = {}
+
+-- Specify a nullary idle() callback to be called whenever there are no ready
+-- fibers but there are waiting fibers. The idle() callback is responsible for
+-- changing zero or more waiting fibers to ready fibers by calling
+-- fiber.wake(), although a given call may leave them all still waiting.
+-- When there are no ready fibers, it's a good idea for the idle() function to
+-- return control to a higher-level execution agent. Simply returning without
+-- changing any fiber's status will spin the CPU.
+-- The idle() callback can return non-nil to exit fiber.run() with that value.
+function fiber._idle()
+ error('fiber.yield(): you must first call set_idle(nullary idle() function)')
+end
+
+function fiber.set_idle(func)
+ fiber._idle = func
+end
+
+-- Launch a new Lua fiber, ready to run.
+function fiber.launch(name, func, ...)
+ local args = table.pack(...)
+ local co = coroutine.create(function() func(table.unpack(args)) end)
+ -- a new fiber is ready to run
+ table.insert(ready, co)
+ local namekey = name
+ while byname[namekey] do
+ if not suffix[name] then
+ suffix[name] = 1
+ end
+ suffix[name] += 1
+ namekey = name .. tostring(suffix[name])
+ end
+ -- found a namekey not yet in byname: set it
+ byname[namekey] = co
+ -- and remember it as this fiber's name
+ names[co] = namekey
+-- dbg('launch(%s)', namekey)
+-- dbg('byname[%s] = %s', namekey, tostring(byname[namekey]))
+-- dbg('names[%s] = %s', tostring(co), names[co])
+-- dbg('ready[-1] = %s', tostring(ready[#ready]))
+end
+
+-- for debugging
+function format_all()
+ output = {}
+ table.insert(output, 'Ready fibers:' .. if next(ready) then '' else ' none')
+ for _, co in pairs(ready) do
+ table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co)))
+ end
+ table.insert(output, 'Waiting fibers:' .. if next(waiting) then '' else ' none')
+ for co in pairs(waiting) do
+ table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co)))
+ end
+ return table.concat(output, '\n')
+end
+
+function fiber.print_all()
+ print(format_all())
+end
+
+-- return either the running coroutine or, if called from the main thread,
+-- 'main'
+function fiber.running()
+ return coroutine.running() or 'main'
+end
+
+-- Query a fiber's name (nil for the running fiber)
+function fiber.get_name(co)
+ return names[co or fiber.running()] or 'unknown'
+end
+
+-- Query status of the passed fiber
+function fiber.status(co)
+ local running = coroutine.running()
+ if (not co) or co == running then
+ -- silly to ask the status of the running fiber: it's 'running'
+ return 'running'
+ end
+ if co ~= 'main' then
+ -- for any coroutine but main, consult coroutine.status()
+ local status = coroutine.status(co)
+ if status ~= 'suspended' then
+ return status
+ end
+ -- here co is suspended, answer needs further refinement
+ else
+ -- co == 'main'
+ if not running then
+ -- asking about 'main' from the main fiber
+ return 'running'
+ end
+ -- asking about 'main' from some other fiber, so presumably main is suspended
+ end
+ -- here we know co is suspended -- but is it ready to run?
+ if waiting[co] then
+ return 'waiting'
+ end
+ -- not waiting should imply ready: sanity check
+ if table.find(ready, co) then
+ return 'ready'
+ end
+ -- Calls within yield() between popping the next ready fiber and
+ -- re-appending it to the list are in this state. Once we're done
+ -- debugging yield(), we could reinstate either of the below.
+-- error(string.format('fiber.status(%s) is stumped', fiber.get_name(co)))
+-- print(string.format('*** fiber.status(%s) is stumped', fiber.get_name(co)))
+ return '(unknown)'
+end
+
+-- change the running fiber's status to waiting
+local function set_waiting()
+ -- if called from the main fiber, inject a 'main' marker into the list
+ co = fiber.running()
+ -- delete from ready list
+ local i = table.find(ready, co)
+ if i then
+ table.remove(ready, i)
+ end
+ -- add to waiting list
+ waiting[co] = true
+end
+
+-- Suspend the current fiber until some other fiber calls fiber.wake() on it
+function fiber.wait()
+ dbg('Fiber %q waiting', fiber.get_name())
+ set_waiting()
+ -- now yield to other fibers
+ fiber.yield()
+end
+
+-- Mark a suspended fiber as being ready to run
+function fiber.wake(co)
+ if not waiting[co] then
+ error(string.format('fiber.wake(%s) but status=%s, ready=%s, waiting=%s',
+ names[co], fiber.status(co), ready[co], waiting[co]))
+ end
+ -- delete from waiting list
+ waiting[co] = nil
+ -- add to end of ready list
+ table.insert(ready, co)
+ dbg('Fiber %q ready', fiber.get_name(co))
+ -- but don't yet resume it: that happens next time we reach yield()
+end
+
+-- pop and return the next not-dead fiber in the ready list, or nil if none remain
+local function live_ready_iter()
+ -- don't write:
+ -- for co in table.remove, ready, 1
+ -- because it would keep passing a new second parameter!
+ for co in function() return table.remove(ready, 1) end do
+ dbg('%s live_ready_iter() sees %s, status %s',
+ fiber.get_name(), fiber.get_name(co), fiber.status(co))
+ -- keep removing the head entry until we find one that's not dead,
+ -- discarding any dead coroutines along the way
+ if co == 'main' or coroutine.status(co) ~= 'dead' then
+ dbg('%s live_ready_iter() returning %s',
+ fiber.get_name(), fiber.get_name(co))
+ return co
+ end
+ end
+ dbg('%s live_ready_iter() returning nil', fiber.get_name())
+ return nil
+end
+
+-- prune the set of waiting fibers
+local function prune_waiting()
+ for waiter in pairs(waiting) do
+ if waiter ~= 'main' and coroutine.status(waiter) == 'dead' then
+ waiting[waiter] = nil
+ end
+ end
+end
+
+-- Run other ready fibers, leaving this one ready, returning after a cycle.
+-- Returns:
+-- * true, nil if there remain other live fibers, whether ready or waiting,
+-- but it's our turn to run
+-- * false, nil if this is the only remaining fiber
+-- * nil, x if configured idle() callback returns non-nil x
+local function scheduler()
+ dbg('scheduler():\n%s', format_all())
+ -- scheduler() is asymmetric because Lua distinguishes the main thread
+ -- from other coroutines. The main thread can't yield; it can only resume
+ -- other coroutines. So although an arbitrary coroutine could resume still
+ -- other arbitrary coroutines, it could NOT resume the main thread because
+ -- the main thread can't yield. Therefore, scheduler() delegates its real
+ -- processing to the main thread. If called from a coroutine, pass control
+ -- back to the main thread.
+ if coroutine.running() then
+ -- this is a real coroutine, yield normally to main thread
+ coroutine.yield()
+ -- main certainly still exists
+ return true
+ end
+
+ -- This is the main fiber: coroutine.yield() doesn't work.
+ -- Instead, resume each of the ready fibers.
+ -- Prune the set of waiting fibers after every time fiber business logic
+ -- runs (i.e. other fibers might have terminated or hit error), such as
+ -- here on entry.
+ prune_waiting()
+ local others, idle_stop
+ repeat
+ for co in live_ready_iter do
+ -- seize the opportunity to make sure the viewer isn't shutting down
+ LL.check_stop()
+ -- before we re-append co, is it the only remaining entry?
+ others = next(ready)
+ -- co is live, re-append it to the ready list
+ table.insert(ready, co)
+ if co == 'main' then
+ -- Since we know the caller is the main fiber, it's our turn.
+ -- Tell caller if there are other ready or waiting fibers.
+ return others or next(waiting)
+ end
+ -- not main, but some other ready coroutine:
+ -- use coro.resume() so we'll propagate any error encountered
+ coro.resume(co)
+ prune_waiting()
+ end
+ -- Here there are no ready fibers. Are there any waiting fibers?
+ if not next(waiting) then
+ return false
+ end
+ -- there are waiting fibers: call consumer's configured idle() function
+ idle_stop = fiber._idle()
+ if idle_stop ~= nil then
+ return nil, idle_stop
+ end
+ prune_waiting()
+ -- loop "forever", that is, until:
+ -- * main is ready, or
+ -- * there are neither ready fibers nor waiting fibers, or
+ -- * fiber._idle() returned non-nil
+ until false
+end
+
+-- Let other fibers run. This is useful in either of two cases:
+-- * fiber.wait() calls this to run other fibers while this one is waiting.
+-- fiber.yield() (and therefore fiber.wait()) works from the main thread as
+-- well as from explicitly-launched fibers, without the caller having to
+-- care.
+-- * A long-running fiber that doesn't often call fiber.wait() should sprinkle
+-- in fiber.yield() calls to interleave processing on other fibers.
+function fiber.yield()
+ -- The difference between this and fiber.run() is that fiber.yield()
+ -- assumes its caller has work to do. yield() returns to its caller as
+ -- soon as scheduler() pops this fiber from the ready list. fiber.run()
+ -- continues looping until all other fibers have terminated, or the
+ -- set_idle() callback tells it to stop.
+ local others, idle_done = scheduler()
+ -- scheduler() returns either if we're ready, or if idle_done ~= nil.
+ if idle_done ~= nil then
+ -- Returning normally from yield() means the caller can carry on with
+ -- its pending work. But in this case scheduler() returned because the
+ -- configured set_idle() function interrupted it -- not because we're
+ -- actually ready. Don't return normally.
+ error('fiber.set_idle() interrupted yield() with: ' .. tostring(idle_done))
+ 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.
+-- Or until configured idle() callback returns x ~= nil: return x.
+function fiber.run()
+ -- A fiber calling run() is not also doing other useful work. Remove the
+ -- calling fiber from the ready list. Otherwise yield() would keep seeing
+ -- that our caller is ready and return to us, instead of realizing that
+ -- all coroutines are waiting and call idle(). But don't say we're
+ -- waiting, either, because then when all other fibers have terminated
+ -- we'd call idle() forever waiting for something to make us ready again.
+ local i = table.find(ready, fiber.running())
+ if i then
+ table.remove(ready, i)
+ end
+ local others, idle_done
+ repeat
+ dbg('%s calling fiber.run() calling scheduler()', fiber.get_name())
+ others, idle_done = scheduler()
+ dbg("%s fiber.run()'s scheduler() returned %s, %s", fiber.get_name(),
+ tostring(others), tostring(idle_done))
+ until (not others)
+ dbg('%s fiber.run() done', fiber.get_name())
+ -- For whatever it's worth, put our own fiber back in the ready list.
+ table.insert(ready, fiber.running())
+ -- Once there are no more waiting fibers, and the only ready fiber is
+ -- us, return to caller. All previously-launched fibers are done. Possibly
+ -- the chunk is done, or the chunk may decide to launch a new batch of
+ -- fibers.
+ return idle_done
+end
+
+-- Make sure we finish up with a call to run(). That allows a consuming script
+-- to kick off some number of fibers, do some work on the main thread and then
+-- fall off the end of the script without explicitly calling fiber.run().
+-- run() ensures the rest of the fibers run to completion (or error).
+LL.atexit(fiber.run)
+
+return fiber
diff --git a/indra/newview/scripts/lua/require/inspect.lua b/indra/newview/scripts/lua/require/inspect.lua
new file mode 100644
index 0000000000..9900a0b81b
--- /dev/null
+++ b/indra/newview/scripts/lua/require/inspect.lua
@@ -0,0 +1,371 @@
+local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table
+local inspect = {Options = {}, }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+inspect._VERSION = 'inspect.lua 3.1.0'
+inspect._URL = 'http://github.com/kikito/inspect.lua'
+inspect._DESCRIPTION = 'human-readable representations of tables'
+inspect._LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2022 Enrique García Cota
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+]]
+inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end })
+inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end })
+
+local tostring = tostring
+local rep = string.rep
+local match = string.match
+local char = string.char
+local gsub = string.gsub
+local fmt = string.format
+
+local _rawget
+if rawget then
+ _rawget = rawget
+else
+ _rawget = function(t, k) return t[k] end
+end
+
+local function rawpairs(t)
+ return next, t, nil
+end
+
+
+
+local function smartQuote(str)
+ if match(str, '"') and not match(str, "'") then
+ return "'" .. str .. "'"
+ end
+ return '"' .. gsub(str, '"', '\\"') .. '"'
+end
+
+
+local shortControlCharEscapes = {
+ ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
+ ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127",
+}
+local longControlCharEscapes = { ["\127"] = "\127" }
+for i = 0, 31 do
+ local ch = char(i)
+ if not shortControlCharEscapes[ch] then
+ shortControlCharEscapes[ch] = "\\" .. i
+ longControlCharEscapes[ch] = fmt("\\%03d", i)
+ end
+end
+
+local function escape(str)
+ return (gsub(gsub(gsub(str, "\\", "\\\\"),
+ "(%c)%f[0-9]", longControlCharEscapes),
+ "%c", shortControlCharEscapes))
+end
+
+local luaKeywords = {
+ ['and'] = true,
+ ['break'] = true,
+ ['do'] = true,
+ ['else'] = true,
+ ['elseif'] = true,
+ ['end'] = true,
+ ['false'] = true,
+ ['for'] = true,
+ ['function'] = true,
+ ['goto'] = true,
+ ['if'] = true,
+ ['in'] = true,
+ ['local'] = true,
+ ['nil'] = true,
+ ['not'] = true,
+ ['or'] = true,
+ ['repeat'] = true,
+ ['return'] = true,
+ ['then'] = true,
+ ['true'] = true,
+ ['until'] = true,
+ ['while'] = true,
+}
+
+local function isIdentifier(str)
+ return type(str) == "string" and
+ not not str:match("^[_%a][_%a%d]*$") and
+ not luaKeywords[str]
+end
+
+local flr = math.floor
+local function isSequenceKey(k, sequenceLength)
+ return type(k) == "number" and
+ flr(k) == k and
+ 1 <= (k) and
+ k <= sequenceLength
+end
+
+local defaultTypeOrders = {
+ ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
+ ['function'] = 5, ['userdata'] = 6, ['thread'] = 7,
+}
+
+local function sortKeys(a, b)
+ local ta, tb = type(a), type(b)
+
+
+ if ta == tb and (ta == 'string' or ta == 'number') then
+ return (a) < (b)
+ end
+
+ local dta = defaultTypeOrders[ta] or 100
+ local dtb = defaultTypeOrders[tb] or 100
+
+
+ return dta == dtb and ta < tb or dta < dtb
+end
+
+local function getKeys(t)
+
+ local seqLen = 1
+ while _rawget(t, seqLen) ~= nil do
+ seqLen = seqLen + 1
+ end
+ seqLen = seqLen - 1
+
+ local keys, keysLen = {}, 0
+ for k in rawpairs(t) do
+ if not isSequenceKey(k, seqLen) then
+ keysLen = keysLen + 1
+ keys[keysLen] = k
+ end
+ end
+ table.sort(keys, sortKeys)
+ return keys, keysLen, seqLen
+end
+
+local function countCycles(x, cycles)
+ if type(x) == "table" then
+ if cycles[x] then
+ cycles[x] = cycles[x] + 1
+ else
+ cycles[x] = 1
+ for k, v in rawpairs(x) do
+ countCycles(k, cycles)
+ countCycles(v, cycles)
+ end
+ countCycles(getmetatable(x), cycles)
+ end
+ end
+end
+
+local function makePath(path, a, b)
+ local newPath = {}
+ local len = #path
+ for i = 1, len do newPath[i] = path[i] end
+
+ newPath[len + 1] = a
+ newPath[len + 2] = b
+
+ return newPath
+end
+
+
+local function processRecursive(process,
+ item,
+ path,
+ visited)
+ if item == nil then return nil end
+ if visited[item] then return visited[item] end
+
+ local processed = process(item, path)
+ if type(processed) == "table" then
+ local processedCopy = {}
+ visited[item] = processedCopy
+ local processedKey
+
+ for k, v in rawpairs(processed) do
+ processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
+ if processedKey ~= nil then
+ processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
+ end
+ end
+
+ local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
+ if type(mt) ~= 'table' then mt = nil end
+ setmetatable(processedCopy, mt)
+ processed = processedCopy
+ end
+ return processed
+end
+
+local function puts(buf, str)
+ buf.n = buf.n + 1
+ buf[buf.n] = str
+end
+
+
+
+local Inspector = {}
+
+
+
+
+
+
+
+
+
+
+local Inspector_mt = { __index = Inspector }
+
+local function tabify(inspector)
+ puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
+end
+
+function Inspector:getId(v)
+ local id = self.ids[v]
+ local ids = self.ids
+ if not id then
+ local tv = type(v)
+ id = (ids[tv] or 0) + 1
+ ids[v], ids[tv] = id, id
+ end
+ return tostring(id)
+end
+
+function Inspector:putValue(v)
+ local buf = self.buf
+ local tv = type(v)
+ if tv == 'string' then
+ puts(buf, smartQuote(escape(v)))
+ elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
+ tv == 'cdata' or tv == 'ctype' then
+ puts(buf, tostring(v))
+ elseif tv == 'table' and not self.ids[v] then
+ local t = v
+
+ if t == inspect.KEY or t == inspect.METATABLE then
+ puts(buf, tostring(t))
+ elseif self.level >= self.depth then
+ puts(buf, '{...}')
+ else
+ if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end
+
+ local keys, keysLen, seqLen = getKeys(t)
+
+ puts(buf, '{')
+ self.level = self.level + 1
+
+ for i = 1, seqLen + keysLen do
+ if i > 1 then puts(buf, ',') end
+ if i <= seqLen then
+ puts(buf, ' ')
+ self:putValue(t[i])
+ else
+ local k = keys[i - seqLen]
+ tabify(self)
+ if isIdentifier(k) then
+ puts(buf, k)
+ else
+ puts(buf, "[")
+ self:putValue(k)
+ puts(buf, "]")
+ end
+ puts(buf, ' = ')
+ self:putValue(t[k])
+ end
+ end
+
+ local mt = getmetatable(t)
+ if type(mt) == 'table' then
+ if seqLen + keysLen > 0 then puts(buf, ',') end
+ tabify(self)
+ puts(buf, '<metatable> = ')
+ self:putValue(mt)
+ end
+
+ self.level = self.level - 1
+
+ if keysLen > 0 or type(mt) == 'table' then
+ tabify(self)
+ elseif seqLen > 0 then
+ puts(buf, ' ')
+ end
+
+ puts(buf, '}')
+ end
+
+ else
+ puts(buf, fmt('<%s %d>', tv, self:getId(v)))
+ end
+end
+
+
+
+
+function inspect.inspect(root, options)
+ options = options or {}
+
+ local depth = options.depth or (math.huge)
+ local newline = options.newline or '\n'
+ local indent = options.indent or ' '
+ local process = options.process
+
+ if process then
+ root = processRecursive(process, root, {}, {})
+ end
+
+ local cycles = {}
+ countCycles(root, cycles)
+
+ local inspector = setmetatable({
+ buf = { n = 0 },
+ ids = {},
+ cycles = cycles,
+ depth = depth,
+ level = 0,
+ newline = newline,
+ indent = indent,
+ }, Inspector_mt)
+
+ inspector:putValue(root)
+
+ return table.concat(inspector.buf)
+end
+
+setmetatable(inspect, {
+ __call = function(_, root, options)
+ return inspect.inspect(root, options)
+ end,
+})
+
+return inspect
diff --git a/indra/newview/scripts/lua/require/leap.lua b/indra/newview/scripts/lua/require/leap.lua
new file mode 100644
index 0000000000..82f91ce9e9
--- /dev/null
+++ b/indra/newview/scripts/lua/require/leap.lua
@@ -0,0 +1,550 @@
+-- Lua implementation of LEAP (LLSD Event API Plugin) protocol
+--
+-- This module supports Lua scripts run by the Second Life viewer.
+--
+-- LEAP protocol passes LLSD objects, converted to/from Lua tables, in both
+-- directions. A typical LLSD object is a map containing keys 'pump' and
+-- 'data'.
+--
+-- The viewer's Lua post_to(pump, data) function posts 'data' to the
+-- LLEventPump 'pump'. This is typically used to engage an LLEventAPI method.
+--
+-- Similarly, the viewer gives each Lua script its own LLEventPump with a
+-- unique name. That name is returned by get_event_pumps(). Every event
+-- received on that LLEventPump is queued for retrieval by get_event_next(),
+-- which returns (pump, data): the name of the LLEventPump on which the event
+-- was received and the received event data. When the queue is empty,
+-- get_event_next() blocks the calling Lua script until the next event is
+-- received.
+--
+-- Usage:
+-- 1. Launch some number of Lua coroutines. The code in each coroutine may
+-- call leap.send(), leap.request() or leap.generate(). leap.send() returns
+-- immediately ("fire and forget"). leap.request() blocks the calling
+-- coroutine until it receives and returns the viewer's response to its
+-- request. leap.generate() expects an arbitrary number of responses to the
+-- original request.
+-- 2. To handle events from the viewer other than direct responses to
+-- requests, instantiate a leap.WaitFor object with a filter(pump, data)
+-- override method that returns non-nil for desired events. A coroutine may
+-- call wait() on any such WaitFor.
+-- 3. Once the coroutines have been launched, call leap.process() on the main
+-- coroutine. process() retrieves incoming events from the viewer and
+-- dispatches them to waiting request() or generate() calls, or to
+-- appropriate WaitFor instances. process() returns when either
+-- get_event_next() raises an error or the viewer posts nil to the script's
+-- reply pump to indicate it's done.
+-- 4. Alternatively, a running coroutine may call leap.done() to break out of
+-- leap.process(). process() won't notice until the next event from the
+-- viewer, though.
+
+local fiber = require('fiber')
+local ErrorQueue = require('ErrorQueue')
+local inspect = require('inspect')
+local function dbg(...) end
+-- local dbg = require('printf')
+local util = require('util')
+
+local leap = {}
+
+-- reply: string name of reply LLEventPump. Any events the viewer posts to
+-- this pump will be queued for get_event_next(). We usually specify it as the
+-- reply pump for requests to internal viewer services.
+-- command: string name of command LLEventPump. post_to(command, ...)
+-- engages LLLeapListener operations such as listening on a specified other
+-- LLEventPump, etc.
+local reply, command = LL.get_event_pumps()
+-- Dict of features added to the LEAP protocol since baseline implementation.
+-- Before engaging a new feature that might break an older viewer, we can
+-- check for the presence of that feature key. This table is solely about the
+-- LEAP protocol itself, the way we communicate with the viewer. To discover
+-- whether a given listener exists, or supports a particular operation, use
+-- command's "getAPI" operation.
+-- For Lua, command's "getFeatures" operation suffices?
+-- leap._features = {}
+
+-- Each outstanding request() or generate() call has a corresponding
+-- WaitForReqid object (later in this module) to handle the
+-- response(s). If an incoming event contains an echoed ["reqid"] key,
+-- we can look up the appropriate WaitForReqid object more efficiently
+-- in a dict than by tossing such objects into the usual waitfors list.
+-- Note: the ["reqid"] must be unique, otherwise we could end up
+-- replacing an earlier WaitForReqid object in pending with a
+-- later one. That means that no incoming event will ever be given to
+-- the old WaitForReqid object. Any coroutine waiting on the discarded
+-- WaitForReqid object would therefore wait forever.
+-- 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.
+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.
+-- Anyone who instantiates a WaitFor subclass object should retain a reference
+-- 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'}
+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
+-- an integer.
+leap._reqid = 0
+-- break leap.process() loop
+leap._done = false
+
+-- get the name of the reply pump
+function leap.replypump()
+ return reply
+end
+
+-- get the name of the command pump
+function leap.cmdpump()
+ return command
+end
+
+-- Fire and forget. Send the specified request LLSD, expecting no reply.
+-- In fact, should the request produce an eventual reply, it will be
+-- treated as an unsolicited event.
+--
+-- See also request(), generate().
+function leap.send(pump, data, reqid)
+ local data = data
+ if type(data) == 'table' then
+ data = table.clone(data)
+ data['reply'] = reply
+ if reqid ~= nil then
+ data['reqid'] = reqid
+ end
+ end
+ dbg('leap.send(%s, %s) calling post_on()', pump, data)
+ LL.post_on(pump, data)
+end
+
+-- common setup code shared by request() and generate()
+local function requestSetup(pump, data)
+ -- invent a new, unique reqid
+ leap._reqid += 1
+ local reqid = leap._reqid
+ -- Instantiate a new WaitForReqid object. The priority is irrelevant
+ -- 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(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)
+ leap.send(pump, data, reqid)
+ return reqid, waitfor
+end
+
+-- Send the specified request LLSD, expecting exactly one reply. Block
+-- the calling coroutine until we receive that reply.
+--
+-- Every request() (or generate()) LLSD block we send will get stamped
+-- with a distinct ["reqid"] value. The requested event API must echo the
+-- same ["reqid"] field in each reply associated with that request. This way
+-- we can correctly dispatch interleaved replies from different requests.
+--
+-- If the desired event API doesn't support the ["reqid"] echo convention,
+-- you should use send() instead -- since request() or generate() would
+-- wait forever for a reply stamped with that ["reqid"] -- and intercept
+-- any replies using WaitFor.
+--
+-- Unless the request data already contains a ["reply"] key, we insert
+-- reply=self.replypump to try to ensure that the expected reply will be
+-- returned over the socket.
+--
+-- See also send(), generate().
+function leap.request(pump, data)
+ local reqid, waitfor = requestSetup(pump, data)
+ dbg('leap.request(%s, %s) about to wait on %s', pump, data, tostring(waitfor))
+ local ok, response = pcall(waitfor.wait, waitfor)
+ dbg('leap.request(%s, %s) got %s: %s', pump, data, ok, response)
+ -- kill off temporary WaitForReqid object, even if error
+ pending[reqid] = nil
+ if not ok then
+ error(response)
+ elseif response.error then
+ error(response.error)
+ else
+ return response
+ end
+end
+
+-- Send the specified request LLSD, expecting an arbitrary number of replies.
+-- 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)
+ 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
+ }
+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.
+ -- 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(table.clone(waitfors)) do
+ waitfor:close()
+ end
+end
+
+-- Handle an incoming (pump, data) event with no recognizable ['reqid']
+local function unsolicited(pump, data)
+ -- we maintain waitfors in descending priority order, so the first waitfor
+ -- to claim this event is the one with the highest priority
+ for i, waitfor in pairs(waitfors) do
+ dbg('unsolicited() checking %s', waitfor.name)
+ if waitfor:handle(pump, data) then
+ return
+ end
+ end
+ 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.
+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
+
+-- We configure fiber.set_idle() function. fiber.yield() calls the configured
+-- idle callback whenever there are waiting fibers but no ready fibers. In
+-- our case, that means it's time to fetch another incoming viewer event.
+fiber.set_idle(function ()
+ -- If someone has called leap.done(), then tell fiber.yield() to break loop.
+ if leap._done then
+ cleanup('done')
+ return 'done'
+ end
+ dbg('leap.idle() calling get_event_next()')
+ local ok, pump, data = pcall(LL.get_event_next)
+ dbg('leap.idle() got %s: %s, %s', ok, pump, data)
+ -- ok false means get_event_next() raised a Lua error, pump is message
+ if not ok then
+ cleanup(pump)
+ error(pump)
+ end
+ -- data nil means get_event_next() returned (pump, LLSD()) to indicate done
+ if not data then
+ cleanup('end')
+ return 'end'
+ end
+ -- got a real pump, data pair
+ dispatch(pump, data)
+ -- return to fiber.yield(): any incoming message might result in one or
+ -- more fibers becoming ready
+end)
+
+function leap.done()
+ leap._done = true
+end
+
+-- called by WaitFor.enable()
+local function registerWaitFor(waitfor)
+ table.insert(waitfors, waitfor)
+ -- keep waitfors sorted in descending order of specified priority
+ table.sort(waitfors,
+ function (lhs, rhs) return lhs.priority > rhs.priority end)
+end
+
+-- called by WaitFor.disable()
+local function unregisterWaitFor(waitfor)
+ local i = table.find(waitfors, waitfor)
+ if i ~= nil then
+ waitfors[i] = nil
+ end
+end
+
+-- ******************************************************************************
+-- WaitFor and friends
+-- ******************************************************************************
+
+-- An unsolicited event is handled by the highest-priority WaitFor subclass
+-- object willing to accept it. If no such object is found, the unsolicited
+-- event is discarded.
+--
+-- * First, instantiate a WaitFor subclass object to register its interest in
+-- some incoming event(s). WaitFor instances are self-registering; merely
+-- instantiating the object suffices.
+-- * Any coroutine may call a given WaitFor object's wait() method. This blocks
+-- the calling coroutine until a suitable event arrives.
+-- * WaitFor's constructor accepts a float priority. Every incoming event
+-- (other than those claimed by request() or generate()) is passed to each
+-- extant WaitFor.filter() method in descending priority order. The first
+-- such filter() to return nontrivial data claims that event.
+-- * At that point, the blocked wait() call on that WaitFor object returns the
+-- item returned by filter().
+-- * WaitFor contains a queue. Multiple arriving events claimed by that WaitFor
+-- object's filter() method are added to the queue. Naturally, until the
+-- queue is empty, calling wait() immediately returns the front entry.
+--
+-- It's reasonable to instantiate a WaitFor subclass whose filter() method
+-- unconditionally returns the incoming event, and whose priority places it
+-- last in the list. This object will enqueue every unsolicited event left
+-- unclaimed by other WaitFor subclass objects.
+--
+-- It's not strictly necessary to associate a WaitFor object with exactly one
+-- coroutine. You might have multiple "worker" coroutines drawing from the same
+-- WaitFor object, useful if the work being done per event might itself involve
+-- "blocking" operations. Or a given coroutine might sample a number of WaitFor
+-- objects in round-robin fashion... etc. etc. Nonetheless, it's
+-- straightforward to designate one coroutine for each WaitFor object.
+
+-- --------------------------------- WaitFor ---------------------------------
+leap.WaitFor = { _id=0 }
+
+function leap.WaitFor.tostring(self)
+ -- Lua (sub)classes have no name; can't prefix with that
+ return self.name
+end
+
+function leap.WaitFor:new(priority, name)
+ local obj = setmetatable({__tostring=leap.WaitFor.tostring}, self)
+ self.__index = self
+
+ obj.priority = priority
+ if name then
+ obj.name = name
+ else
+ self._id += 1
+ obj.name = 'WaitFor' .. self._id
+ end
+ obj._queue = ErrorQueue()
+ obj._registered = false
+ -- if no priority, then don't enable() - remember 0 is truthy
+ if priority then
+ obj:enable()
+ end
+
+ 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()
+ if not self._registered then
+ registerWaitFor(self)
+ self._registered = true
+ end
+end
+
+-- Disable an enable()d WaitFor object.
+function leap.WaitFor:disable()
+ if self._registered then
+ unregisterWaitFor(self)
+ self._registered = false
+ end
+end
+
+-- Block the calling coroutine until a suitable unsolicited event (one
+-- for which filter() returns the event) arrives.
+function leap.WaitFor:wait()
+ dbg('%s about to wait', self.name)
+ local item = self._queue:Dequeue()
+ dbg('%s got %s', self.name, item)
+ return item
+end
+
+-- Override filter() to examine the incoming event in whatever way
+-- makes sense.
+--
+-- Return nil to ignore this event.
+--
+-- To claim the event, return the item you want placed in the queue.
+-- Typically you'd write:
+-- return data
+-- or perhaps
+-- return {pump=pump, data=data}
+-- or some variation.
+function leap.WaitFor:filter(pump, data)
+ error('You must override the WaitFor.filter() method')
+end
+
+-- called by unsolicited() for each WaitFor in waitfors
+function leap.WaitFor:handle(pump, data)
+ local item = self:filter(pump, data)
+ dbg('%s.filter() returned %s', self.name, item)
+ -- if this item doesn't pass the filter, we're not interested
+ if not item then
+ return false
+ end
+ -- okay, filter() claims this event
+ self:process(item)
+ return true
+end
+
+-- called by WaitFor:handle() for an accepted event
+function leap.WaitFor:process(item)
+ self._queue:Enqueue(item)
+end
+
+-- called by cleanup() at end
+function leap.WaitFor:close()
+ self:disable()
+ self._queue:close()
+end
+
+-- called by leap.process() when get_event_next() raises an error
+function leap.WaitFor:exception(message)
+ LL.print_warning(self.name .. ' error: ' .. message)
+ self._queue:Error(message)
+end
+
+-- ------------------------------ WaitForReqid -------------------------------
+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(nil, 'WaitForReqid(' .. reqid .. ')')
+ setmetatable(obj, self)
+ self.__index = self
+
+ obj.reqid = 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
+ -- to test the event again. Accept every such event.
+ 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/require/login.lua b/indra/newview/scripts/lua/require/login.lua
new file mode 100644
index 0000000000..37c9093a21
--- /dev/null
+++ b/indra/newview/scripts/lua/require/login.lua
@@ -0,0 +1,42 @@
+local leap = require 'leap'
+local startup = require 'startup'
+local mapargs = require 'mapargs'
+
+local login = {}
+
+local function ensure_login_state(op)
+ -- no point trying to login until the viewer is ready
+ startup.wait('STATE_LOGIN_WAIT')
+ -- Once we've actually started login, LLPanelLogin is destroyed, and so is
+ -- its "LLPanelLogin" listener. At that point,
+ -- leap.request("LLPanelLogin", ...) will hang indefinitely because no one
+ -- is listening on that LLEventPump any more. Intercept that case and
+ -- produce a sensible error.
+ local state = startup.state()
+ if startup.before('STATE_LOGIN_WAIT', state) then
+ error(`Can't engage login operation {op} once we've reached state {state}`, 2)
+ end
+end
+
+local function fullgrid(grid)
+ if string.find(grid, '.', 1, true) then
+ return grid
+ else
+ return `util.{grid}.secondlife.com`
+ end
+end
+
+function login.login(...)
+ ensure_login_state('login')
+ local args = mapargs('username,grid,slurl', ...)
+ args.op = 'login'
+ args.grid = fullgrid(args.grid)
+ return leap.request('LLPanelLogin', args)
+end
+
+function login.savedLogins(grid)
+ ensure_login_state('savedLogins')
+ return leap.request('LLPanelLogin', {op='savedLogins', grid=fullgrid(grid)})['logins']
+end
+
+return login
diff --git a/indra/newview/scripts/lua/require/logout.lua b/indra/newview/scripts/lua/require/logout.lua
new file mode 100644
index 0000000000..63dcd7f01f
--- /dev/null
+++ b/indra/newview/scripts/lua/require/logout.lua
@@ -0,0 +1,7 @@
+local leap = require 'leap'
+
+local function logout()
+ leap.send('LLAppViewer', {op='userQuit'});
+end
+
+return logout
diff --git a/indra/newview/scripts/lua/require/mapargs.lua b/indra/newview/scripts/lua/require/mapargs.lua
new file mode 100644
index 0000000000..45f5a9c556
--- /dev/null
+++ b/indra/newview/scripts/lua/require/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/require/printf.lua b/indra/newview/scripts/lua/require/printf.lua
new file mode 100644
index 0000000000..e84b2024df
--- /dev/null
+++ b/indra/newview/scripts/lua/require/printf.lua
@@ -0,0 +1,19 @@
+-- printf(...) is short for print(string.format(...))
+
+local inspect = require 'inspect'
+
+local function printf(format, ...)
+ -- string.format() only handles numbers and strings.
+ -- Convert anything else to string using the inspect module.
+ local args = {}
+ for _, arg in pairs(table.pack(...)) do
+ if type(arg) == 'number' or type(arg) == 'string' then
+ table.insert(args, arg)
+ else
+ table.insert(args, inspect(arg))
+ end
+ end
+ print(string.format(format, table.unpack(args)))
+end
+
+return printf
diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua
new file mode 100644
index 0000000000..5301d7838c
--- /dev/null
+++ b/indra/newview/scripts/lua/require/result_view.lua
@@ -0,0 +1,98 @@
+local leap = require 'leap'
+
+-- metatable for every result_view() table
+local mt = {
+ __len = function(self)
+ return self.length
+ end,
+ __index = function(self, i)
+ -- right away, convert to 0-relative indexing
+ i -= 1
+ -- can we find this index within the current slice?
+ local reli = i - self.start
+ if 0 <= reli and reli < #self.slice then
+ -- Lua 1-relative indexing
+ return self.slice[reli + 1]
+ end
+ -- is this index outside the overall result set?
+ if not (0 <= i and i < self.length) then
+ return nil
+ end
+ -- fetch a new slice starting at i, using provided fetch()
+ local start
+ self.slice, start = self.fetch(self.key, i)
+ -- It's possible that caller-provided fetch() function forgot
+ -- to return the adjusted start index of the new slice. In
+ -- Lua, 0 tests as true, so if fetch() returned (slice, 0),
+ -- we'll duly reset self.start to 0. Otherwise, assume the
+ -- requested index was not adjusted: that the returned slice
+ -- really does start at i.
+ self.start = start or i
+ -- Hopefully this slice contains the desired i.
+ -- Back to 1-relative indexing.
+ return self.slice[i - self.start + 1]
+ end,
+ -- We purposely avoid putting any array entries (int keys) into
+ -- our table so that access to any int key will always call our
+ -- __index() metamethod. Moreover, we want any table iteration to
+ -- call __index(table, i) however many times; we do NOT want it to
+ -- retrieve key, length, start, slice.
+ -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'.
+ __iter = ipairs,
+ -- This result set provides read-only access.
+ -- We do not support pushing updates to individual items back to
+ -- C++; for the intended use cases, that makes no sense.
+ __newindex = function(self, i, value)
+ error("result_view is a read-only data structure", 2)
+ end
+}
+
+-- result_view(key_length, fetch) returns a table which stores only a slice
+-- of a result set plus some control values, yet presents read-only virtual
+-- access to the entire result set.
+-- key_length: {result set key, total result set length}
+-- fetch: function(key, start) that returns (slice, adjusted start)
+local result_view = setmetatable(
+ {
+ -- generic fetch() function
+ fetch = function(key, start)
+ local fetched = leap.request(
+ 'LLInventory',
+ {op='getSlice', result=key, index=start})
+ return fetched.slice, fetched.start
+ end,
+ -- generic close() function accepting variadic result-set keys
+ close = function(...)
+ local keys = table.pack(...)
+ -- table.pack() produces a table with an array entry for every
+ -- parameter, PLUS an 'n' key with the count. Unfortunately that
+ -- 'n' key bollixes our conversion to LLSD, which requires either
+ -- all int keys (for an array) or all string keys (for a map).
+ keys.n = nil
+ leap.send('LLInventory', {op='closeResult', result=keys})
+ end
+ },
+ {
+ -- result_view(key_length, fetch) calls this
+ __call = function(class, key_length, fetch)
+ return setmetatable(
+ {
+ key=key_length[1],
+ length=key_length[2],
+ -- C++ result sets use 0-based indexing, so internally we do too
+ start=0,
+ -- start with a dummy array with length 0
+ slice={},
+ -- if caller didn't pass fetch() function, use generic
+ fetch=fetch or class.fetch,
+ -- returned view:close() will close result set with passed key
+ close=function(self) class.close(key_length[1]) end
+ },
+ -- use our special metatable
+ mt
+ )
+ end
+ }
+)
+
+return result_view
diff --git a/indra/newview/scripts/lua/require/startup.lua b/indra/newview/scripts/lua/require/startup.lua
new file mode 100644
index 0000000000..c3040f94b8
--- /dev/null
+++ b/indra/newview/scripts/lua/require/startup.lua
@@ -0,0 +1,100 @@
+-- query, wait for or mandate a particular viewer startup state
+
+-- During startup, the viewer steps through a sequence of numbered (and named)
+-- states. This can be used to detect when, for instance, the login screen is
+-- displayed, or when the viewer has finished logging in and is fully
+-- in-world.
+
+local fiber = require 'fiber'
+local leap = require 'leap'
+local inspect = require 'inspect'
+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']
+
+local byname = setmetatable(
+ {},
+ -- set metatable to throw an error if you look up invalid state name
+ {__index=function(t, k)
+ local v = rawget(t, k)
+ if v then
+ return v
+ end
+ error(string.format('startup module passed invalid state %q', k), 2)
+ end})
+
+-- derive byname as a lookup table to find the 0-based index for a given name
+for i, name in pairs(bynum) do
+ -- the viewer's states are 0-based, not 1-based like Lua indexes
+ byname[name] = i - 1
+end
+-- dbg('startup states: %s', inspect(byname))
+
+-- specialize a WaitFor to track the viewer's startup state
+local startup_pump = 'StartupState'
+local waitfor = leap.WaitFor(0, startup_pump)
+function waitfor:filter(pump, data)
+ if pump == self.name then
+ return data
+ end
+end
+
+function waitfor:process(data)
+ -- keep updating startup._state for interested parties
+ startup._state = data.str
+ dbg('startup updating state to %q', data.str)
+ -- now pass data along to base-class method to queue
+ leap.WaitFor.process(self, data)
+end
+
+-- listen for StartupState events
+leap.request(leap.cmdpump(),
+ {op='listen', source=startup_pump, listener='startup.lua', tweak=true})
+-- poke LLStartUp to make sure we get an event
+leap.send('LLStartUp', {op='postStartupState'})
+
+-- ---------------------------------------------------------------------------
+-- wait for response from postStartupState
+while not startup._state do
+ dbg('startup.state() waiting for first StartupState event')
+ waitfor:wait()
+end
+
+-- return a list of all known startup states
+function startup.list()
+ return bynum
+end
+
+-- report whether state with string name 'left' is before string name 'right'
+function startup.before(left, right)
+ return byname[left] < byname[right]
+end
+
+-- report the viewer's current startup state
+function startup.state()
+ return startup._state
+end
+
+-- error if script is called before specified state string name
+function startup.ensure(state)
+ if startup.before(startup.state(), state) then
+ -- tell error() to pretend this error was thrown by our caller
+ error('must not be called before startup state ' .. state, 2)
+ end
+end
+
+-- block calling fiber until viewer has reached state with specified string name
+function startup.wait(state)
+ dbg('startup.wait(%q)', state)
+ while startup.before(startup.state(), state) do
+ local item = waitfor:wait()
+ dbg('startup.wait(%q) sees %s', state, item)
+ end
+end
+
+return startup
diff --git a/indra/newview/scripts/lua/require/timers.lua b/indra/newview/scripts/lua/require/timers.lua
new file mode 100644
index 0000000000..ab1615ffbf
--- /dev/null
+++ b/indra/newview/scripts/lua/require/timers.lua
@@ -0,0 +1,122 @@
+-- Access to the viewer's time-delay facilities
+
+local leap = require 'leap'
+local util = require 'util'
+
+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 calls = 0
+ if iterate then
+ -- With iterative timers, beware of running a timer callback which
+ -- performs async actions lasting longer than the timer interval. The
+ -- lengthy callback suspends, allowing leap to retrieve the next
+ -- event, which is a timer tick. leap calls a new instance of the
+ -- callback, even though the previous callback call is still
+ -- suspended... etc. 'in_callback' defends against that recursive
+ -- case. Rather than re-enter the suspended callback, drop the
+ -- too-soon timer event. (We could count the too-soon timer events and
+ -- iterate calling the callback, but it's a bathtub problem: the
+ -- callback could end up getting farther and farther behind.)
+ local in_callback = false
+ obj.id = leap.eventstream(
+ 'Timers',
+ {op='scheduleEvery', every=delay},
+ function (event)
+ local reqid = event.reqid
+ calls += 1
+ if calls == 1 then
+ dbg('timer(%s) first callback', reqid)
+ -- discard the first (immediate) response: don't call callback
+ return nil
+ else
+ if in_callback then
+ dbg('dropping timer(%s) callback %d', reqid, calls)
+ else
+ dbg('timer(%s) callback %d', reqid, calls)
+ in_callback = true
+ local ret = callback(event)
+ in_callback = false
+ return ret
+ end
+ end
+ end
+ ).reqid
+ else -- (not iterate)
+ obj.id = leap.eventstream(
+ 'Timers',
+ {op='scheduleAfter', after=delay},
+ function (event)
+ calls += 1
+ -- 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 calls == 1 then
+ -- 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
+
+util.classctor(timers.Timer)
+
+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
diff --git a/indra/newview/scripts/lua/require/util.lua b/indra/newview/scripts/lua/require/util.lua
new file mode 100644
index 0000000000..40737a159a
--- /dev/null
+++ b/indra/newview/scripts/lua/require/util.lua
@@ -0,0 +1,114 @@
+-- utility functions, in alpha order
+
+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)
+ -- set class's __call metamethod to the specified constructor function
+ -- (class.new if not specified)
+ util.setmetamethods{class, __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
+end
+
+-- reliable count of the number of entries in table t
+-- (since #t is unreliable)
+function util.count(t)
+ local count = 0
+ for _ in pairs(t) do
+ count += 1
+ end
+ return count
+end
+
+-- cheap test whether table t is empty
+function util.empty(t)
+ return not next(t)
+end
+
+-- recursive table equality
+function util.equal(t1, t2)
+ if not (type(t1) == 'table' and type(t2) == 'table') then
+ return t1 == t2
+ end
+ -- both t1 and t2 are tables: get modifiable copy of t2
+ local temp = table.clone(t2)
+ for k, v in pairs(t1) do
+ -- if any key in t1 doesn't have same value in t2, not equal
+ if not util.equal(v, temp[k]) then
+ return false
+ end
+ -- temp[k] == t1[k], delete temp[k]
+ temp[k] = nil
+ end
+ -- All keys in t1 have equal values in t2; t2 == t1 if there are no extra keys in t2
+ return util.empty(temp)
+end
+
+-- Find or create the metatable for a specified table (a new empty table if
+-- omitted), and to that metatable assign the specified keys.
+-- Setting multiple keys at once is more efficient than a function to set only
+-- one at a time, e.g. setametamethod().
+-- t = util.setmetamethods{__index=readfunc, __len=lenfunc}
+-- returns a new table with specified metamethods __index, __len
+-- util.setmetamethods{t, __call=action}
+-- finds or creates the metatable for existing table t and sets __call
+-- util.setmetamethods{table=t, __call=action}
+-- same as util.setmetamethods{t, __call=action}
+function util.setmetamethods(specs)
+ -- first determine the target table
+ assert(not (specs.table and specs[1]),
+ "Pass setmetamethods table either as positional or table=, not both")
+ local t = specs.table or specs[1] or {}
+ -- remove both ways of specifying table, leaving only the metamethods
+ specs.table = nil
+ specs[1] = nil
+ local mt = getmetatable(t)
+ if not mt then
+ -- t doesn't already have a metatable: just set specs
+ setmetatable(t, specs)
+ else
+ -- t already has a metatable: copy specs into it
+ local key, value
+ for key, value in pairs(specs) do
+ mt[key] = value
+ end
+ end
+ -- having set or enriched t's metatable, return t
+ return t
+end
+
+-- On the passed module (i.e. table), set an __index metamethod such that
+-- referencing module.submodule lazily requires(path/submodule).
+-- The loaded submodule is cached in the module table so it need not be passed
+-- to require() again.
+-- 'path', like any require() string, can be relative to LuaRequirePath.
+-- Returns the enriched module, permitting e.g.
+-- mymod = util.submoduledir({}, 'mymod')
+function util.submoduledir(module, path)
+ return util.setmetamethods{
+ module,
+ __index=function(t, key)
+ local mod = require(`{path}/{key}`)
+ -- cache the submodule
+ t[key] = mod
+ return mod
+ end
+ }
+end
+
+return util
diff --git a/indra/newview/scripts/lua/test_LLAppearance.lua b/indra/newview/scripts/lua/test_LLAppearance.lua
new file mode 100644
index 0000000000..a97ec4e0ca
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLAppearance.lua
@@ -0,0 +1,114 @@
+local LLAppearance = require 'LLAppearance'
+local startup = require 'startup'
+local inspect = require 'inspect'
+local UI = require 'UI'
+
+local SHOW_OUTFITS = true
+local SELECTED_OUTFIT_ID = {}
+local DATA_MAP = {}
+
+local wearables_lbl = 'Show wearables'
+local outfits_lbl = 'Show outfits'
+local replace_cof_lbl = 'Replace COF'
+local add_cof_lbl = 'Add to COF'
+local outfits_title = 'Outfits'
+local wear_lbl = 'Wear item'
+local detach_lbl = 'Detach item'
+
+local flt = UI.Floater(
+ "luafloater_outfits_list.xml",
+ {outfits_list = {"double_click"}})
+
+function get_selected_id()
+ return flt:request({action="get_selected_id", ctrl_name='outfits_list'}).value
+end
+
+function populate_list()
+ if SHOW_OUTFITS then
+ DATA_MAP = LLAppearance.getOutfitsList()
+ else
+ DATA_MAP = LLAppearance.getOutfitItems(SELECTED_OUTFIT_ID)
+ end
+
+ local action_data = {}
+ action_data.action = "add_list_element"
+ action_data.ctrl_name = "outfits_list"
+ local outfits = {}
+ for uuid, info in pairs(DATA_MAP) do
+ name = {}
+ if SHOW_OUTFITS then
+ name = info
+ else
+ name = info.name
+ end
+ table.insert(outfits, {value = uuid, columns={column = "outfit_name", value = name}})
+ end
+ action_data.value = outfits
+ flt:post(action_data)
+end
+
+function set_label(btn_name, value)
+ flt:post({action="set_label", ctrl_name=btn_name, value=value})
+end
+
+function set_enabled(btn_name, value)
+ flt:post({action="set_enabled", ctrl_name=btn_name, value=value})
+end
+
+function update_labels()
+ if SHOW_OUTFITS then
+ set_label('select_btn', wearables_lbl)
+ set_label('replace_btn', replace_cof_lbl)
+ set_label('add_btn', add_cof_lbl)
+
+ set_enabled('select_btn', false)
+ flt:post({action="set_title", value=outfits_title})
+ else
+ set_label('select_btn', outfits_lbl)
+ set_label('replace_btn', wear_lbl)
+ set_label('add_btn', detach_lbl)
+
+ set_enabled('select_btn', true)
+ flt:post({action="set_title", value=DATA_MAP[SELECTED_OUTFIT_ID]})
+ end
+
+ set_enabled('replace_btn', false)
+ set_enabled('add_btn', false)
+end
+
+function flt:post_build(event_data)
+ populate_list()
+end
+
+function flt:commit_replace_btn(event_data)
+ if SHOW_OUTFITS then
+ LLAppearance.wearOutfit(get_selected_id(), 'replace')
+ else
+ LLAppearance.wearItems(get_selected_id(), false)
+ end
+end
+
+function flt:commit_add_btn(event_data)
+ if SHOW_OUTFITS then
+ LLAppearance.wearOutfit(get_selected_id(), 'add')
+ else
+ LLAppearance.detachItems(get_selected_id())
+ end
+end
+
+function flt:commit_select_btn(event_data)
+ SHOW_OUTFITS = not SHOW_OUTFITS
+ SELECTED_OUTFIT_ID = get_selected_id()
+ update_labels()
+ self:post({action="clear_list", ctrl_name='outfits_list'})
+ populate_list()
+end
+
+function flt:commit_outfits_list(event_data)
+ set_enabled('replace_btn', true)
+ set_enabled('add_btn', true)
+ set_enabled('select_btn', true)
+end
+
+startup.wait('STATE_STARTED')
+flt:show()
diff --git a/indra/newview/scripts/lua/test_LLChat.lua b/indra/newview/scripts/lua/test_LLChat.lua
new file mode 100644
index 0000000000..3abaf28e42
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLChat.lua
@@ -0,0 +1,18 @@
+LLChat = require 'LLChat'
+
+function generateRandomWord(length)
+ local alphabet = "abcdefghijklmnopqrstuvwxyz"
+ local wordTable = {}
+ for i = 1, length do
+ local randomIndex = math.random(1, #alphabet)
+ table.insert(wordTable, alphabet:sub(randomIndex, randomIndex))
+ end
+ return table.concat(wordTable)
+end
+
+local msg = {'AI says:'}
+math.randomseed(os.time())
+for i = 1, math.random(1, 10) do
+ table.insert(msg, generateRandomWord(math.random(1, 8)))
+end
+LLChat.sendNearby(table.concat(msg, ' '))
diff --git a/indra/newview/scripts/lua/test_LLChatListener.lua b/indra/newview/scripts/lua/test_LLChatListener.lua
new file mode 100644
index 0000000000..0f269b54e6
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLChatListener.lua
@@ -0,0 +1,39 @@
+local LLListener = require 'LLListener'
+local LLChat = require 'LLChat'
+local UI = require 'UI'
+
+-- Chat listener script allows to use the following commands in Nearby chat:
+-- open inventory -- open defined floater by name
+-- close inventory -- close defined floater by name
+-- closeall -- close all floaters
+-- stop -- close the script
+-- any other messages will be echoed.
+function openOrEcho(message)
+ local open_floater_name = string.match(message, "^open%s+(%w+)")
+ local close_floater_name = string.match(message, "^close%s+(%w+)")
+ if open_floater_name then
+ UI.showFloater(open_floater_name)
+ elseif close_floater_name then
+ UI.hideFloater(close_floater_name)
+ elseif message == 'closeall' then
+ UI.closeAllFloaters()
+ else
+ LLChat.sendNearby('Echo: ' .. message)
+ end
+end
+
+local listener = LLListener(LLChat.nearbyChatPump)
+
+function listener:handleMessages(event_data)
+ if string.find(event_data.message, '[LUA]') then
+ return true
+ elseif event_data.message == 'stop' then
+ LLChat.sendNearby('Closing echo script.')
+ return false
+ else
+ openOrEcho(event_data.message)
+ end
+ return true
+end
+
+listener:start()
diff --git a/indra/newview/scripts/lua/test_LLFloaterAbout.lua b/indra/newview/scripts/lua/test_LLFloaterAbout.lua
new file mode 100644
index 0000000000..6bbf61982d
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLFloaterAbout.lua
@@ -0,0 +1,6 @@
+-- test LLFloaterAbout
+
+LLFloaterAbout = require('LLFloaterAbout')
+inspect = require('inspect')
+
+print(inspect(LLFloaterAbout.getInfo()))
diff --git a/indra/newview/scripts/lua/test_LLGesture.lua b/indra/newview/scripts/lua/test_LLGesture.lua
new file mode 100644
index 0000000000..1cce674565
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLGesture.lua
@@ -0,0 +1,26 @@
+-- exercise LLGesture API
+
+LLGesture = require 'LLGesture'
+inspect = require 'inspect'
+
+
+-- getActiveGestures() returns {<UUID>: {name, playing, trigger}}
+gestures_uuid = LLGesture.getActiveGestures()
+-- convert to {<name>: <uuid>}
+gestures = {}
+for uuid, info in pairs(gestures_uuid) do
+ gestures[info.name] = uuid
+end
+-- now run through the list
+for name, uuid in pairs(gestures) do
+ if name == 'afk' then
+ -- afk has a long timeout, and isn't interesting to look at
+ continue
+ end
+ print(name)
+ LLGesture.startGesture(uuid)
+ repeat
+ LL.sleep(1)
+ until not LLGesture.isGesturePlaying(uuid)
+end
+print('Done.')
diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua
new file mode 100644
index 0000000000..de57484bcd
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLInventory.lua
@@ -0,0 +1,24 @@
+inspect = require 'inspect'
+LLInventory = require 'LLInventory'
+
+-- Get 'My Landmarks' folder id (you can see all folder types via LLInventory.getFolderTypeNames())
+my_landmarks_id = LLInventory.getBasicFolderID('landmark')
+-- Get 3 landmarks from the 'My Landmarks' folder (you can see all folder types via LLInventory.getAssetTypeNames())
+landmarks = LLInventory.collectDescendentsIf{folder_id=my_landmarks_id, type="landmark", limit=3}
+for _, landmark in pairs(landmarks.items) do
+ print(landmark.name)
+end
+
+-- Get 'Calling Cards' folder id
+calling_cards_id = LLInventory.getBasicFolderID('callcard')
+-- Get all items located directly in 'Calling Cards' folder
+calling_cards = LLInventory.getDirectDescendents(calling_cards_id).items
+
+-- Print a random calling card name from 'Calling Cards' folder
+-- (because getDirectDescendents().items is a Lua result set, selecting
+-- a random entry only fetches one slice containing that entry)
+math.randomseed(os.time())
+for i = 1, 5 do
+ pick = math.random(#calling_cards)
+ print(`Random calling card (#{pick} of {#calling_cards}): {calling_cards[pick].name}`)
+end
diff --git a/indra/newview/scripts/lua/test_animation.lua b/indra/newview/scripts/lua/test_animation.lua
new file mode 100644
index 0000000000..37e7254a6c
--- /dev/null
+++ b/indra/newview/scripts/lua/test_animation.lua
@@ -0,0 +1,32 @@
+LLInventory = require 'LLInventory'
+LLAgent = require 'LLAgent'
+
+-- Get 'Animations' folder id (you can see all folder types via LLInventory.getFolderTypeNames())
+animations_id = LLInventory.getBasicFolderID('animatn')
+-- Get animations from the 'Animation' folder (you can see all folder types via LLInventory.getAssetTypeNames())
+anims = LLInventory.collectDescendentsIf{folder_id=animations_id, type="animatn"}.items
+
+local anim_ids = {}
+for key in pairs(anims) do
+ table.insert(anim_ids, key)
+end
+
+if #anim_ids == 0 then
+ print("No animations found")
+else
+ -- Start playing a random animation
+ math.randomseed(os.time())
+ local random_id = anim_ids[math.random(#anim_ids)]
+ local anim_info = LLAgent.getAnimationInfo(random_id)
+
+ print("Starting animation locally: " .. anims[random_id].name)
+ print("Loop: " .. anim_info.is_loop .. " Joints: " .. anim_info.num_joints .. " Duration " .. tonumber(string.format("%.2f", anim_info.duration)))
+ LLAgent.playAnimation{item_id=random_id}
+
+ -- Stop animation after 3 sec if it's looped or longer than 3 sec
+ if anim_info.is_loop == 1 or anim_info.duration > 3 then
+ LL.sleep(3)
+ print("Stop animation.")
+ LLAgent.stopAnimation(random_id)
+ end
+end
diff --git a/indra/newview/scripts/lua/test_atexit.lua b/indra/newview/scripts/lua/test_atexit.lua
new file mode 100644
index 0000000000..6fbc0f3eb1
--- /dev/null
+++ b/indra/newview/scripts/lua/test_atexit.lua
@@ -0,0 +1,3 @@
+LL.atexit(function() print('Third') end)
+LL.atexit(function() print('Second') end)
+LL.atexit(function() print('First') end)
diff --git a/indra/newview/scripts/lua/test_autopilot.lua b/indra/newview/scripts/lua/test_autopilot.lua
new file mode 100644
index 0000000000..09c85c140a
--- /dev/null
+++ b/indra/newview/scripts/lua/test_autopilot.lua
@@ -0,0 +1,22 @@
+local LLAgent = require 'LLAgent'
+local LLListener = require 'LLListener'
+
+local pos = LLAgent.getGlobalPosition()
+pos[1]+=10 -- delta x
+pos[2]+=5 -- delta y
+LLAgent.requestStand()
+LLAgent.startAutoPilot{target_global=pos,allow_flying=false,stop_distance=1}
+
+local listener = LLListener(LLAgent.autoPilotPump)
+
+function listener:handleMessages(event_data)
+ if event_data.success then
+ print('Destination is reached')
+ LLAgent.requestSit()
+ else
+ print('Failed to reach destination')
+ end
+ return false
+end
+
+listener:start()
diff --git a/indra/newview/scripts/lua/test_callables.lua b/indra/newview/scripts/lua/test_callables.lua
new file mode 100644
index 0000000000..1bee062db8
--- /dev/null
+++ b/indra/newview/scripts/lua/test_callables.lua
@@ -0,0 +1,6 @@
+startup=require 'startup'
+UI=require 'UI'
+startup.wait('STATE_LOGIN_WAIT')
+for _, cbl in pairs(UI.callables()) do
+ print(`{cbl.name} ({cbl.access})`)
+end
diff --git a/indra/newview/scripts/lua/test_camera_control.lua b/indra/newview/scripts/lua/test_camera_control.lua
new file mode 100644
index 0000000000..7ac0986ee6
--- /dev/null
+++ b/indra/newview/scripts/lua/test_camera_control.lua
@@ -0,0 +1,49 @@
+local LLAgent = require 'LLAgent'
+local startup = require 'startup'
+local UI = require 'UI'
+
+local flt = UI.Floater('luafloater_camera_control.xml')
+
+function getValue(ctrl_name)
+ return flt:request({action="get_value", ctrl_name=ctrl_name}).value
+end
+
+function setValue(ctrl_name, value)
+ flt:post({action="set_value", ctrl_name=ctrl_name, value=value})
+end
+
+function flt:commit_update_btn(event_data)
+ lock_focus = getValue('lock_focus_ctrl')
+ lock_camera = getValue('lock_camera_ctrl')
+ local camera_pos = {getValue('cam_x'), getValue('cam_y'), getValue('cam_z')}
+ local focus_pos = {getValue('focus_x'), getValue('focus_y'), getValue('focus_z')}
+
+ LLAgent.setCamera{camera_pos=camera_pos, focus_pos=focus_pos,
+ focus_locked=lock_focus, camera_locked=lock_camera}
+
+ self:post({action="add_text", ctrl_name="events_editor",
+ value = {'Updating FollowCam params', 'camera_pos:', camera_pos, 'focus_pos:', focus_pos,
+ 'lock_focus:', lock_focus, 'lock_camera:', lock_camera}})
+end
+
+function flt:commit_agent_cam_btn(event_data)
+ agent_pos = LLAgent.getRegionPosition()
+ setValue('cam_x', math.floor(agent_pos[1]))
+ setValue('cam_y', math.floor(agent_pos[2]))
+ setValue('cam_z', math.floor(agent_pos[3]))
+end
+
+function flt:commit_agent_focus_btn(event_data)
+ agent_pos = LLAgent.getRegionPosition()
+ setValue('focus_x', math.floor(agent_pos[1]))
+ setValue('focus_y', math.floor(agent_pos[2]))
+ setValue('focus_z', math.floor(agent_pos[3]))
+end
+
+function flt:commit_reset_btn(event_data)
+ LLAgent.removeCamParams()
+ LLAgent.setFollowCamActive(false)
+end
+
+startup.wait('STATE_STARTED')
+flt:show()
diff --git a/indra/newview/scripts/lua/test_flycam.lua b/indra/newview/scripts/lua/test_flycam.lua
new file mode 100644
index 0000000000..05c3c37b93
--- /dev/null
+++ b/indra/newview/scripts/lua/test_flycam.lua
@@ -0,0 +1,38 @@
+-- Make camera fly around the subject avatar for a few seconds.
+
+local LLAgent = require 'LLAgent'
+local startup = require 'startup'
+local timers = require 'timers'
+
+local height = 2.0 -- meters
+local radius = 4.0 -- meters
+local speed = 1.0 -- meters/second along circle
+local start = os.clock()
+local stop = os.clock() + 30 -- seconds
+
+local function cameraPos(t)
+ local agent = LLAgent.getRegionPosition()
+ local radians = speed * t
+ return {
+ agent[1] + radius * math.cos(radians),
+ agent[2] + radius * math.sin(radians),
+ agent[3] + height
+ }
+end
+
+local function moveCamera()
+ if os.clock() < stop then
+ -- usual case
+ LLAgent.setCamera{ camera_pos=cameraPos(os.clock() - start), camera_locked=true }
+ return nil
+ else
+ -- last time
+ LLAgent.removeCamParams()
+ LLAgent.setFollowCamActive(false)
+ return true
+ end
+end
+
+startup.wait('STATE_STARTED')
+-- call moveCamera() repeatedly until it returns true
+local timer = timers.Timer(0.1, moveCamera, true)
diff --git a/indra/newview/scripts/lua/test_group_chat.lua b/indra/newview/scripts/lua/test_group_chat.lua
new file mode 100644
index 0000000000..373411c26c
--- /dev/null
+++ b/indra/newview/scripts/lua/test_group_chat.lua
@@ -0,0 +1,14 @@
+LLChat = require 'LLChat'
+LLAgent = require 'LLAgent'
+UI = require 'UI'
+
+local GROUPS = LLAgent.getGroups()
+
+-- Choose one of the groups randomly and send group message
+math.randomseed(os.time())
+group_info = GROUPS[math.random(#GROUPS)]
+LLChat.startGroupChat(group_info.id)
+response = UI.popup:alertYesCancel('Started group chat with ' .. group_info.name .. ' group. Send greetings?')
+if response == 'OK' then
+ LLChat.sendGroupIM('Greetings', group_info.id)
+end
diff --git a/indra/newview/scripts/lua/test_inv_resultset.lua b/indra/newview/scripts/lua/test_inv_resultset.lua
new file mode 100644
index 0000000000..c31cfe3c67
--- /dev/null
+++ b/indra/newview/scripts/lua/test_inv_resultset.lua
@@ -0,0 +1,18 @@
+local LLInventory = require 'LLInventory'
+local inspect = require 'inspect'
+
+print('basic folders:')
+print(inspect(LLInventory.getFolderTypeNames()))
+
+local folder = LLInventory.getBasicFolderID('my_otfts')
+print(`folder = {folder}`)
+local result = LLInventory.getDirectDescendants(folder)
+print(`type(result) = {type(result)}`)
+print(#result.categories, 'categories:')
+for i, cat in pairs(result.categories) do
+ print(`{i}: {cat.name}`)
+end
+print(#result.items, 'items')
+for i, item in pairs(result.items) do
+ print(`{i}: {item.name}`)
+end
diff --git a/indra/newview/scripts/lua/test_login.lua b/indra/newview/scripts/lua/test_login.lua
new file mode 100644
index 0000000000..54d3635a41
--- /dev/null
+++ b/indra/newview/scripts/lua/test_login.lua
@@ -0,0 +1,10 @@
+inspect = require 'inspect'
+login = require 'login'
+
+local grid = 'agni'
+print(inspect(login.savedLogins(grid)))
+
+print(inspect(login.login{
+ username='Your Username',
+ grid=grid
+ }))
diff --git a/indra/newview/scripts/lua/test_logout.lua b/indra/newview/scripts/lua/test_logout.lua
new file mode 100644
index 0000000000..b1ac59e38c
--- /dev/null
+++ b/indra/newview/scripts/lua/test_logout.lua
@@ -0,0 +1,3 @@
+logout = require 'logout'
+
+logout()
diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua
new file mode 100644
index 0000000000..2158134511
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_demo.lua
@@ -0,0 +1,39 @@
+local leap = require 'leap'
+local startup = require 'startup'
+local UI = require 'UI'
+
+local flt = UI.Floater(
+ 'luafloater_demo.xml',
+ {show_time_lbl = {"right_mouse_down", "double_click"}})
+
+-- override base-class handleEvents() to report the event data in the floater's display field
+function flt:handleEvents(event_data)
+ self:post({action="add_text", ctrl_name="events_editor", value = event_data})
+ -- forward the call to base-class handleEvents()
+ return UI.Floater.handleEvents(self, event_data)
+end
+
+function flt:commit_disable_ctrl(event_data)
+ self:post({action="set_enabled", ctrl_name="open_btn", value = (1 - event_data.value)})
+end
+
+function flt:commit_title_cmb(event_data)
+ self:post({action="set_title", value=event_data.value})
+end
+
+function flt:commit_open_btn(event_data)
+ floater_name = self:request({action="get_value", ctrl_name='openfloater_cmd'}).value
+ leap.send("LLFloaterReg", {name = floater_name, op = "showInstance"})
+end
+
+local function getCurrentTime()
+ local currentTime = os.date("*t")
+ return string.format("%02d:%02d:%02d", currentTime.hour, currentTime.min, currentTime.sec)
+end
+
+function flt:double_click_show_time_lbl(event_data)
+ self:post({action="set_value", ctrl_name="time_lbl", value=getCurrentTime()})
+end
+
+startup.wait('STATE_LOGIN_WAIT')
+flt:show()
diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
new file mode 100644
index 0000000000..5f929c0d0c
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
@@ -0,0 +1,27 @@
+local LLGesture = require 'LLGesture'
+local startup = require 'startup'
+local UI = require 'UI'
+
+local flt = UI.Floater(
+ "luafloater_gesture_list.xml",
+ {gesture_list = {"double_click"}})
+
+function flt:post_build(event_data)
+ local gestures_uuid = LLGesture.getActiveGestures()
+ local action_data = {}
+ action_data.action = "add_list_element"
+ action_data.ctrl_name = "gesture_list"
+ local gestures = {}
+ for uuid, info in pairs(gestures_uuid) do
+ table.insert(gestures, {value = uuid, columns={column = "gesture_name", value = info.name}})
+ end
+ action_data.value = gestures
+ self:post(action_data)
+end
+
+function flt:double_click_gesture_list(event_data)
+ LLGesture.startGesture(event_data.value)
+end
+
+startup.wait('STATE_STARTED')
+flt:show()
diff --git a/indra/newview/scripts/lua/test_luafloater_speedometer.lua b/indra/newview/scripts/lua/test_luafloater_speedometer.lua
new file mode 100644
index 0000000000..2cdd41fd7e
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_speedometer.lua
@@ -0,0 +1,31 @@
+local leap = require 'leap'
+local startup = require 'startup'
+local Timer = (require 'timers').Timer
+local UI = require 'UI'
+local popup = UI.popup
+local max_speed = 0
+local flt = UI.Floater("luafloater_speedometer.xml")
+startup.wait('STATE_STARTED')
+
+local timer
+
+function flt:floater_close(event_data)
+ if timer then
+ timer:cancel()
+ end
+ popup:tip(string.format("Registered max speed: %.2f m/s", max_speed))
+end
+
+local function idle(event_data)
+ local speed = leap.request('LLVOAvatar', {op='getSpeed'})['value']
+ flt:post({action="set_value", ctrl_name="speed_lbl", value = string.format("%.2f", speed)})
+ max_speed=math.max(max_speed, speed)
+end
+
+msg = 'Are you sure you want to run this "speedometer" script?'
+response = popup:alertYesCancel(msg)
+
+if response == 'OK' then
+ flt:show()
+ 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_popup.lua b/indra/newview/scripts/lua/test_popup.lua
new file mode 100644
index 0000000000..7a11895669
--- /dev/null
+++ b/indra/newview/scripts/lua/test_popup.lua
@@ -0,0 +1,10 @@
+UI = require 'UI'
+popup = UI.popup
+startup = require 'startup'
+
+startup.wait('STATE_STARTED')
+
+response = popup:alert('This just has a Close button')
+response = popup:alertOK(string.format('You said "%s", is that OK?', response))
+response = popup:alertYesCancel(string.format('You said "%s"', response))
+popup:tip(string.format('You said "%s"', response))
diff --git a/indra/newview/scripts/lua/test_result_view.lua b/indra/newview/scripts/lua/test_result_view.lua
new file mode 100644
index 0000000000..304633a472
--- /dev/null
+++ b/indra/newview/scripts/lua/test_result_view.lua
@@ -0,0 +1,55 @@
+-- Verify the functionality of result_view.
+result_view = require 'result_view'
+
+print('alphabet')
+alphabet = "abcdefghijklmnopqrstuvwxyz"
+assert(#alphabet == 26)
+alphabits = string.split(alphabet, '')
+
+print('function slice()')
+function slice(t, index, count)
+ return table.move(t, index, index + count - 1, 1, {})
+end
+
+print('verify slice()')
+-- verify that slice() does what we expect
+assert(table.concat(slice(alphabits, 4, 3)) == "def")
+assert(table.concat(slice(alphabits, 14, 3)) == "nop")
+assert(table.concat(slice(alphabits, 25, 3)) == "yz")
+
+print('function fetch()')
+function fetch(key, index)
+ -- fetch function is defined to be 0-relative: fix for Lua data
+ -- constrain view of alphabits to slices of at most 3 elements
+ return slice(alphabits, index+1, 3), index
+end
+
+print('result_view()')
+-- for test purposes, key is irrelevant, so just 'key'
+view = result_view({'key', #alphabits}, fetch)
+
+print('function check_iter()')
+function check_iter(...)
+ result = {}
+ for k, v in ... do
+ table.insert(result, v)
+ end
+ assert(table.concat(result) == alphabet)
+end
+
+print('check_iter(pairs(view))')
+check_iter(pairs(view))
+print('check_iter(ipairs(view))')
+check_iter(ipairs(view))
+print('check_iter(view)')
+check_iter(view)
+
+print('raw index access')
+assert(view[5] == 'e')
+assert(view[10] == 'j')
+assert(view[15] == 'o')
+assert(view[20] == 't')
+assert(view[25] == 'y')
+
+print('Success!')
+
diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua
new file mode 100644
index 0000000000..ec5cd47e93
--- /dev/null
+++ b/indra/newview/scripts/lua/test_setdtor.lua
@@ -0,0 +1,91 @@
+inspect = require 'inspect'
+
+print('initial setdtor')
+bye = LL.setdtor('initial setdtor', 'Goodbye world!', print)
+
+print('arithmetic')
+n = LL.setdtor('arithmetic', 11, print)
+print("n =", n)
+print("n._target =", n._target)
+print(pcall(function() n._target = 12 end))
+print("getmetatable(n) =", inspect(getmetatable(n)))
+print("-n =", -n)
+for i = 10, 12 do
+ -- Comparison metamethods are only called if both operands have the same
+ -- metamethod.
+ tempi = LL.setdtor('tempi', i, function(n) print('temp', i) end)
+ print(`n < {i}`, n < tempi)
+ print(`n <= {i}`, n <= tempi)
+ print(`n == {i}`, n == tempi)
+ print(`n ~= {i}`, n ~= tempi)
+ print(`n >= {i}`, n >= tempi)
+ print(`n > {i}`, n > tempi)
+end
+i = 2
+print(`n + {i} =`, n + i)
+print(`{i} + n =`, i + n)
+print(`n - {i} =`, n - i)
+print(`{i} - n =`, i - n)
+print(`n * {i} =`, n * i)
+print(`{i} * n =`, i * n)
+print(`n / {i} =`, n / i)
+print(`{i} / n =`, i / n)
+print(`n // {i} =`, n // i)
+print(`{i} // n =`, i // n)
+print(`n % {i} =`, n % i)
+print(`{i} % n =`, i % n)
+print(`n ^ {i} =`, n ^ i)
+print(`{i} ^ n =`, i ^ n)
+
+print('string')
+s = LL.setdtor('string', 'hello', print)
+print('s =', s)
+print('#s =', #s)
+print('s .. " world" =', s .. " world")
+print('"world " .. s =', "world " .. s)
+
+print('table')
+t = LL.setdtor('table', {'[1]', '[2]', abc='.abc', def='.def'},
+ function(t) print(inspect(t)) end)
+print('t =', inspect(t))
+print('t._target =', inspect(t._target))
+print('#t =', #t)
+print('next(t) =', next(t))
+print('next(t, 1) =', next(t, 1))
+print('t[2] =', t[2])
+print('t.def =', t.def)
+t[1] = 'new [1]'
+print('t[1] =', t[1])
+print('for k, v in pairs(t) do')
+for k, v in pairs(t) do
+ print(`{k}: {v}`)
+end
+print('for k, v in ipairs(t) do')
+for k, v in ipairs(t) do
+ print(`{k}: {v}`)
+end
+print('for k, v in t do')
+for k, v in t do
+ print(`{k}: {v}`)
+end
+-- and now for something completely different
+setmetatable(
+ t._target,
+ {
+ __iter = function(arg)
+ return next, {'alternate', '__iter'}
+ end
+ }
+)
+print('for k, v in t with __iter() metamethod do')
+for k, v in t do
+ print(`{k}: {v}`)
+end
+
+print('function')
+f = LL.setdtor('function', function(a, b) return (a .. b) end, print)
+print('f =', f)
+print('f._target =', f._target)
+print('f("Hello", " world") =', f("Hello", " world"))
+
+print('cleanup')
diff --git a/indra/newview/scripts/lua/test_snapshot.lua b/indra/newview/scripts/lua/test_snapshot.lua
new file mode 100644
index 0000000000..d7c878833b
--- /dev/null
+++ b/indra/newview/scripts/lua/test_snapshot.lua
@@ -0,0 +1,15 @@
+local UI = require 'UI'
+
+PATH = 'E:\\'
+-- 'png', 'jpeg' or 'bmp'
+EXT = '.png'
+
+NAME_SIMPLE = 'Snapshot_simple' .. '_' .. os.date("%Y-%m-%d_%H-%M-%S")
+UI.snapshot(PATH .. NAME_SIMPLE .. EXT)
+
+NAME_ARGS = 'Snapshot_args' .. '_' .. os.date("%Y-%m-%d_%H-%M-%S")
+
+-- 'COLOR' or 'DEPTH'
+TYPE = 'COLOR'
+UI.snapshot{PATH .. NAME_ARGS .. EXT, width = 700, height = 400,
+ type = TYPE, showui = false, showhud = false}
diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua
new file mode 100644
index 0000000000..be5001aa16
--- /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(10)')
+start = os.clock()
+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())
+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(5)')
+start = os.clock()
+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(2)')
+start = os.clock()
+t2 = timers.Timer(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(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(
+ 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/test_toolbars.lua b/indra/newview/scripts/lua/test_toolbars.lua
new file mode 100644
index 0000000000..7683fca8a3
--- /dev/null
+++ b/indra/newview/scripts/lua/test_toolbars.lua
@@ -0,0 +1,26 @@
+UI = require 'UI'
+
+local BUTTONS = UI.getToolbarBtnNames()
+local TOOLBARS = {'left','right','bottom'}
+
+-- Clear the toolbars and then add the toolbar buttons to the random toolbar
+response = UI.popup:alertYesCancel('Toolbars will be randomly reshuffled. Proceed?')
+if response == 'OK' then
+ UI.clearAllToolbars()
+ math.randomseed(os.time())
+
+ -- add the buttons to the random toolbar
+ for i = 1, #BUTTONS do
+ UI.addToolbarBtn(BUTTONS[i], TOOLBARS[math.random(3)])
+ end
+
+ -- remove some of the added buttons from the toolbars
+ for i = 1, #BUTTONS do
+ if math.random(100) < 30 then
+ UI.removeToolbarBtn(BUTTONS[i])
+ end
+ end
+ popup:tip('Toolbars were reshuffled')
+else
+ popup:tip('Canceled')
+end
diff --git a/indra/newview/scripts/lua/test_top_menu.lua b/indra/newview/scripts/lua/test_top_menu.lua
new file mode 100644
index 0000000000..f877cda5eb
--- /dev/null
+++ b/indra/newview/scripts/lua/test_top_menu.lua
@@ -0,0 +1,52 @@
+UI = require 'UI'
+
+--Add new drop-down 'LUA Menu' to the Top menu.
+local MENU_NAME = "lua_menu"
+UI.addMenu{name=MENU_NAME,label="LUA Menu"}
+
+--Add two new menu items to the 'LUA Menu': 'Debug console' and 'Scripts'
+UI.addMenuItem{name="lua_debug",label="Debug console",
+ param="lua_debug",
+ func="Floater.ToggleOrBringToFront",
+ parent_menu=MENU_NAME}
+
+UI.addMenuItem{name="lua_scripts",label="Scripts",
+ param="lua_scripts",
+ func="Floater.ToggleOrBringToFront",
+ parent_menu=MENU_NAME}
+
+--Add menu separator to the 'LUA Menu' under added menu items
+UI.addMenuSeparator{parent_menu=MENU_NAME}
+
+--Add 'Demo scripts...' branch to the 'LUA Menu'
+local DEMO_BRANCH = "demo_scripts"
+UI.addMenuBranch{name=DEMO_BRANCH,label="Demo scripts...",parent_menu=MENU_NAME}
+
+--Add menu items to the 'Demo scripts...' branch, which will invoke specified script on click
+UI.addMenuItem{name="speedometer",label="Speedometer",
+ param="test_luafloater_speedometer.lua",
+ func="Lua.RunScript",
+ parent_menu=DEMO_BRANCH}
+
+UI.addMenuItem{name="gesture_list",label="Gesture list",
+ param="test_luafloater_gesture_list.lua",
+ func="Lua.RunScript",
+ parent_menu=DEMO_BRANCH}
+
+--Add one more menu separator
+UI.addMenuSeparator{parent_menu=MENU_NAME}
+
+--Add 'About...' branch to the 'LUA Menu'
+local ABOUT_BRANCH = "about_branch"
+UI.addMenuBranch{name=ABOUT_BRANCH,label="About...",parent_menu=MENU_NAME}
+
+--Add two new menu items to the 'About...' branch
+UI.addMenuItem{name="lua_info",label="Lua...",
+ param="https://www.lua.org/about.html",
+ func="Advanced.ShowURL",
+ parent_menu=ABOUT_BRANCH}
+
+UI.addMenuItem{name="lua_info",label="Luau...",
+ param="https://luau-lang.org/",
+ func="Advanced.ShowURL",
+ parent_menu=ABOUT_BRANCH}
diff --git a/indra/newview/scripts/lua/testmod.lua b/indra/newview/scripts/lua/testmod.lua
new file mode 100644
index 0000000000..60f7f80db1
--- /dev/null
+++ b/indra/newview/scripts/lua/testmod.lua
@@ -0,0 +1,2 @@
+print('loaded scripts/lua/testmod.lua')
+return function () return 'hello from scripts/lua/testmod.lua' end