Cool VL Viewer forum

View unanswered posts | View active topics It is currently 2022-07-04 10:03:00

This topic is locked, you cannot edit posts or make further replies.  [ 8 posts ] 
Lua scripting and viewer automation feature 
Author Message

Joined: 2009-03-17 18:42:51
Posts: 4937
Starting with v1.26.20.4, the Cool VL Viewer implements Lua (Lua v5.4 since v1.28.0.1) scripting/automation support.

EDIT: it became way too tedious for me to maintain a full (60+ pages worth) Lua documentation on this forum, and for you to read it as an immense wall of text...
Please, refer to the PDF manual instead.

There is a specific thread for Lua scripting related feature requests. Please use it to post any such request.

2017-01-14 11:14:35
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
Sample/demonstration automation script (user_settings/automation.lua):
known_ids = {}
known_sessions = {}
tp_started = false
tp_retry = false
max_agents = 20
max_complexity_shield_off = 0
max_complexity_shield_on = 200000
max_area_shield_off = 0
max_area_shield_on = 200
max_memory_shield_off = 0
max_memory_shield_on = 100
derendered_objects = { "253538b0-56fe-1feb-8089-2cade9c5a413", "6b752a3c-c0a8-8df7-0d0e-d95c0cda33a1" }
low_dd_regions = { ["Porten Hill"]=true }
protected_attachments = { "hair", "collar", "cuff", "nipple", "penis", "pussy", "hoof", " ear", "horn", "tail", "muzzle" }
protected_layers = { "shape", "eyes", "hair", "skin", "alpha" }
avatars_colors = {}
account_settings = {}

-- Here, we define a custom set of side bar buttons for performing useful
-- tasks or opening commonly used floaters that do not have an associated
-- toolbar button.
-- We also perform tasks that can only happen after successful login.
function OnLogin(location)
     -- Setup the sidebar
    SideBarButton(1, "\u{2699}", "OpenFloater('preferences')", "Opens the Preferences floater")
    -- Setup the side bar button for spelling language toggling:
    SideBarButton(3, "inv_item_landmark_visited.tga", "OpenFloater('teleport history')", "Opens the Teleport history")
    SideBarButton(4, "inv_item_mesh.tga", "nop", "Toggles mesh queue info")
    SideBarButtonToggle(4, "DebugShowMeshQueue")
    SideBarButton(5, "43f0a590-f3d3-48b5-b460-f5b3e6e03626.tga", "OpenFloater('sounds list')", "Opens the Sounds list floater")
    -- Setup the side bar button for anti-griefers protection toggling:
    -- Setup the side bar button for attachments and layers protection:
    local agent = GetAgentInfo()
    if agent.rlv then
    -- Retrieve our per-account settings and validate them (we use a table for
    -- our settings, since it is much simpler than dealing with serialization of
    -- settings to a data string ourselves).
    local settings = GetPerAccountData()
    if type(settings) == "table" then
        account_settings = settings
    -- Restore last Windlight and water settings, if any
    if account_settings.last_sky then
    if account_settings.last_water then
    -- Derender objects we never want to see
    for i = 1, #derendered_objects, 1 do
     -- Set the draw distance for the login sim, after letting a chance
     -- to the viewer to connect to potential neighbor sims.
    CallbackAfter(5, SetDrawDistance)
     -- Define the Lua pie menu for avatars

-- This callback is invoked each time the side bar visibility is changed. We
-- use it to display the Lua icon in the status bar whenever the side bar gets
-- hidden and we setup the command for that icon so that, when clicked, it
-- shows the side bar (which in turn hides the status bar icon via this
-- callback).
function OnSideBarVisibilityChange(visible)
    if visible then
        StatusBarLuaIcon("SideBarHide(false)", "Shows the Lua buttons side bar")

-- This function toggles the spell checking language between English and
-- French: much easier than doing it via the Preferences menu... Of course,
-- you will have to make sure first that all the related dictionaries have
-- been installed on your system (from "Preferences" -> "Cool features" ->
-- "Chat, IM & text").
function ToggleSpellCheckLanguage(toggle)
    local lang = GetDebugSetting("SpellCheckLanguage")
    if toggle then
        if lang == "fr_fr" then
            lang = "en_us"
            lang = "fr_fr"
        SetDebugSetting("SpellCheckLanguage", lang)
    if lang == "fr_fr" then
        SideBarButton(2, "En", "AutomationMessage('language')", "Toggles the spell checking language to English (US)")
        SideBarButton(2, "Fr", "AutomationMessage('language')", "Toggles the spell checking language to French")

-- This callback receives the messages sent by the various buttons we setup via
-- the AutomationMessage() function.
function OnAutomationMessage(text)
    if text == "shield-on" then
        SetDebugSetting("AllowSwapping", false)
        SetDebugSetting("MainMemorySafetyCheck64", true)
        SetDebugSetting("RenderAvatarMaxComplexity", max_complexity_shield_on)
        SetDebugSetting("RenderAutoMuteSurfaceAreaLimit", max_area_shield_on)
        SetDebugSetting("RenderAutoMuteMemoryLimit", max_memory_shield_on)
        SetDebugSetting("KillBogusObjects", true)
        SideBarButton(6, "shield_off.png", "AutomationMessage('shield-off')", "Disables anti-griefers features")
        OpenNotification(0, "Anti-griefer features enabled")
    elseif text == "shield-off" then
        SetDebugSetting("AllowSwapping", true)
        SetDebugSetting("MainMemorySafetyCheck64", false)
        SetDebugSetting("RenderAvatarMaxComplexity", max_complexity_shield_off)
        SetDebugSetting("RenderAutoMuteSurfaceAreaLimit", max_area_shield_off)
        SetDebugSetting("RenderAutoMuteMemoryLimit", max_memory_shield_off)
        SetDebugSetting("KillBogusObjects", false)
        SideBarButton(6, "shield_on.png", "AutomationMessage('shield-on')", "Enables anti-griefers features")
        OpenNotification(0, "Anti-griefer features disabled")
    elseif text == "cancel-auto-tp" then
        tp_retry = false
        OverlayBarLuaButton("", "")
    elseif text == "language" then
    elseif text == "protect" then
        SideBarButton(8, "9beb8cdd-3dce-53c2-b28e-e1f3bc2ec0a4.tga", "AutomationMessage('unprotect')", "Unlock protected layers and attachments")
        SideBarButtonToggle(8, true)
        OpenNotification(0, "Protected attachments and layers locked")
    elseif text == "unprotect" then
        SideBarButton(8, "9beb8cdd-3dce-53c2-b28e-e1f3bc2ec0a4.tga", "AutomationMessage('protect')", "Lock protected layers and attachments")
        SideBarButtonToggle(8, false)
        OpenNotification(0, "Protected attachments and layers unlocked")

-- This is just a demonstration of how Lua scripting may be used as a mean of
-- defense against griefing attempts: here, when pushed and the anti-griefer
-- measures are on, the avatar is automatically sat down on ground to prevent
-- further effects of new pushes.
function OnAgentPush(id, t, magnitude)
    if shield_on and mag > 3 then
    OpenNotification(0, "Push detected, magnitude = " .. mag)

-- Here we add pseudo commands to:
--  * emulate Firestorm's "/dd" (draw distance) command (with "/dd" alone as an
--    alias for "/dd 256");
--  * toggle the camera front view with "/fc";
--  * adjust the avatar Z offset with "/z " followed by a amount of centimeters
--    (e.g.: "/z -8" to lower your avatar height by 8 cm), with "/z" alone to reset
--    the offset to zero.
-- We also create a pseudo-gesture ("/g " for "greetings") accepting a parameter
-- (the name or title of the resident you want to greet).
function OnSendChat(text)
    if string.sub(text, 1, 1) ~= "/" then
        -- Do not waste time searching for commands if the first character is
        -- not a slash...
        return text
    if text == "/fc" then
        SetDebugSetting("CameraFrontView", not GetDebugSetting("CameraFrontView"))
        return ""
    if text == "/dd" then
        SetDebugSetting("RenderFarClip", 256)
        return ""
    local i, j = string.find(text, "/dd ")
    if i == 1 then
        local distance = tonumber(string.sub(text, j + 1))
        if distance > 512 then
            distance = 512
        elseif distance < 32 then
            distance = 32
        SetDebugSetting("RenderFarClip", distance)
        return ""
    if text == "/z" then
        SetDebugSetting("AvatarOffsetZ", 0)
        return ""
    i, j = string.find(text, "/z ")
    if i == 1 then
        local offset = tonumber(string.sub(text, j + 1)) / 100
        if offset > 9 then
            offset = 9
        elseif offset < -9 then
            offset = 9
        SetDebugSetting("AvatarOffsetZ", offset)
        return ""
    i, j = string.find(text, "/g ")
    if i == 1 then
        text = "/me smiles softly, \"Greetings" .. string.sub(text, j) .. ".\""
    return text

-- This is just a demonstration of SLURL dispatching and floater opening, all
-- wrapped up in an OnReceivedChat() callback: it got no real practical use
-- other than being a sample of what can be done (and may be dangerous to use
-- "as is", due to the SLURL systematic auto-dispatching).
function OnReceivedChat(t, id, is_avatar, name, text)
    local i, j = string.find(text, "secondlife://")
    if i then
        OpenNotification(0, name .. " sent an SURL, dispatching it")
        DispatchSLURL(string.sub(text, i))
    if known_ids[id] then
    known_ids[id] = true
    if is_avatar then
        OpenNotification(0, name .. " is a new chatting avatar: displaying profile.")
        OpenFloater("avatar info", id)
        OpenNotification(0, name .. " is a new chatting object: inspecting it.")
        OpenFloater("inspect", id)

-- Here is a demonstration on how to use the OnChatTextColoring() callback to
-- color incoming chat text from avatars. In this example, the friends' chat
-- text is colored in pink.
function OnChatTextColoring(id, name, text)
    if IsAgentFriend(id) then
        return "pink2"
    return ""

-- This is just a demonstration of IM callbacks usage and of how to use the
-- CallbackAfter() function: it got no real practical use other than being a
-- sample of what can be done.
function InstantMsgReply(session_id, name, text)
    OpenNotification(0, name .. " opened a new IM session: replying now.")
    SendIM(session_id, text)

function OnInstantMsg(session_id, origin_id, t, name, text)
    if known_sessions[session_id] then
    known_sessions[session_id] = true
    -- Wait 3 seconds for the IM session to start (important for group sessions) before replying:
    CallbackAfter(3, InstantMsgReply, session_id, name, "Hello !")

-- This function is used to automatically set the draw distance after TP:
-- If the sim we arrive into is listed in the low_dd_regions table, then the
-- draw distance is set to 128m. If it is a sim without neighbors (island),
-- then the draw distance is set to 512m (to rez everything in sim).
-- In all other cases, the draw distance is set to 256m.
-- Note that the "speed rezzing" feature is also accounted for (i.e. there
-- will not be conflict between this code and the feature: the draw distance
-- final adjustment is done once the speed rezzing adjustments are over).
function SetDrawDistance()
    if GetDebugSetting("SpeedRez") then
        local saved_dd = GetDebugSetting("SavedRenderFarClip")
        if saved_dd > 0 and saved_dd ~= GetDebugSetting("RenderFarClip") then
            CallbackAfter(GetDebugSetting("SpeedRezInterval") + 1, SetDrawDistance)
    local dd = 256
    local location = GetGridSimAndPos()
    local region = location.region
    if low_dd_regions[region] then
        dd = 128
    elseif location.neighbors == 0 then
        dd = 512
    SetDebugSetting("RenderFarClip", dd)

-- The code below allows to auto-retry failed teleports (provided the TP global
-- coordinates were known when the TP was first attempted: this is the case for
-- all TPs done from the world map floater, but may not be the case for landmarks
-- TPs and TP invites, at least on the first TP attempt). It illustrates the use
-- of the Lua dialog and overlay bar button, of the AutomationMessage() function
-- and of the TP related callbacks and functions.
function OnTPStateChange(state, reason)
    if state == 0 then -- TELEPORT_NONE
        tp_retry = false
        if tp_started and string.len(reason) > 0 and
           reason ~= "invalid_tport" and
           reason ~= "nolandmark_tport" and
           reason ~= "noaccess_tport" and
           reason ~= "no_inventory_host" then
            if reason == "no_host" then
                -- Auto-retry after sim comes back online
                tp_retry = true
                OverlayBarLuaButton("Cancel auto-TP", "AutomationMessage('cancel-auto-tp')")
                MakeDialog("Retry TP",
                           "To auto-retry the TP, give the max allowed number of agents for this sim, else press the Cancel button",
                           "Cancel", "", "OK",
                           "DialogClose()", "", "DialogClose()")
            -- Set the draw distance for the arrival sim, after letting a chance
            -- to the viewer to connect to potential neighbor sims.
            CallbackAfter(8, SetDrawDistance)
        tp_started = false
    elseif state == 1 then -- TELEPORT_START
        tp_started = true
        tp_retry = false
    elseif state ~= 2 then -- all other teleport states than TELEPORT_REQUESTED
        tp_started = false
        tp_retry = false

function OnLuaDialogClose(title, button, text)
    if title == "Retry TP" and button == 3 then
        max_agent = tonumbe(text)
        if max_agent > 0 then
            tp_retry = true
            OverlayBarLuaButton("Cancel auto-TP", "AutomationMessage('cancel-auto-tp')")

function OnFailedTPSimChange(agents, x, y, z)
    if tp_retry and agents < max_agents then
        TeleportAgentToPos(x, y, z)
        OverlayBarLuaButton("", "")

-- This is called each time the user loads new Windlight or water presets.
-- We then save the new presets into the per-account settings, so to restore
-- them on next login with the same avatar.
function OnWindlightChange(sky_settings_name, water_settings_name)
    if sky_settings_name ~= "" then
        account_settings.last_sky = sky_settings_name
    if water_settings_name ~= "" then
        account_settings.last_water = water_settings_name

-- Here is an example of how to use the Lua pie menu, the mini-map and tag
-- colors, and the OnAvatarRezzing() callback.
-- This defines an avatar pie menu with colors you can set for the corresponding
-- avatar's dot in the mini-map and name tag. The color is remembered during the
-- session, even if the avatar gets de-rezzed and re-rezzed.
function DefineAvatarPieMenu()
    LuaPieMenuSlice(4, 1, "Blue", "nop")
    LuaPieMenuSlice(4, 2, "Cyan", "nop")
    LuaPieMenuSlice(4, 3, "Red", "nop")
    LuaPieMenuSlice(4, 4, "Magenta", "nop")
    LuaPieMenuSlice(4, 5, "Yellow", "nop")
    LuaPieMenuSlice(4, 6, "White", "nop")
    LuaPieMenuSlice(4, 7, "Default", "nop")
    LuaPieMenuSlice(4, 8, "Green", "nop")

function OnLuaPieMenu(data)
    local color = ""
    if data.slice == 1 then
        color = "blue"
    elseif data.slice == 2 then
        color = "cyan"
    elseif data.slice == 3 then
        color = "red"
    elseif data.slice == 4 then
        color = "magenta"
    elseif data.slice == 5 then
        color = "yellow"
    elseif data.slice == 6 then
        color = "white"
    elseif data.slice == 8 then
        color = "green"
    avatars_colors[data.object_id] = color
    SetAvatarMinimapColor(data.object_id, color)
    SetAvatarNameTagColor(data.object_id, color)

function OnAvatarRezzing(id)
    local color = avatars_colors[id]
    if color then
        SetAvatarMinimapColor(id, color)
        SetAvatarNameTagColor(id, color)

-- This function allows to lock, via RestrainedLove, object attachments and clothing layers
-- based on their name (for attachments) or type (for clothing layers). Only the joints and
-- layers corresponding to protected items currently worn are locked.
function LockAttachmentsAndLayers()
    local i, j, k, v, name
    local protected = {}
    local items = GetAgentWearables();
    for k, v in pairs(items) do
        j = string.find(v, "|")
        name = string.lower(string.sub(v, j + 1))
        for i = 1, #protected_layers, 1 do
            if protected_layers[i] == name then
                protected[name] = true
    for k, v in pairs(protected) do
        ExecuteRLV("remoutfit:" .. k .. "=n")
    protected = {}
    items = GetAgentAttachments()
    for k, v in pairs(items) do
        j = string.find(v, "|")
        name = string.lower(string.sub(v, 0, j - 1))
        for i = 1, #protected_attachments, 1 do
            if string.find(name, protected_attachments[i]) then
                protected[string.sub(v, j + 1)] = true
    for k, v in pairs(protected) do
        ExecuteRLV("remattach:" .. k .. "=n")

2017-01-28 14:53:34
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
Here is what the Lua status bar icon looks like:

And here is an example of a side-bar corresponding to the sample script given in the previous post:

2017-02-13 09:37:11
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
Here is a simple way to emulate Firestorm's parcel Windlight support in the Cool VL Viewer, via Lua (just add/merge to your automation.lua script):
function OnParcelChange(parcel)
    local desc = parcel.description
    -- Search for and extract text enclosed between "/*" and "*/"
    local i, j = string.find(desc, "/*")
    if not i then
    desc = string.sub(desc, j + 1)
    i, j = string.find(desc, "*/")
    if not i then
    desc = string.sub(desc, 1, i - 1)
    -- Match 'Sky<anything but '"'>"<sky_settings_name>"'
    local sky = string.match(desc, 'Sky[^"]*"([^"]+)')
    -- Match 'Water<anything but '"'>"<water_settings_name>"'
    local water = string.match(desc, 'Water[^"]*"([^"]+)')
    -- Apply settings if found
    if sky and sky ~= "" then
    if water and water ~= "" then

  • I only scripted above the "basic" support, without the altitude zoning syntax (it could be added, however): the Windlight settings will therefore be applied whatever the altitude of your avatar.
  • Since the Cool VL Viewer does not include all the default Windlight settings Firestorm got, you will have to add the latter into the user_settings/windlight/ directory, so that the Cool VL Viewer can find them.

2017-03-28 09:18:38
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
Custom Lua floaters are supported via specific functions. Here is an example of how to use them.

First, add a "floater_lua_test.xml" file to your custom skins directory ("~/.secondlife/skins/default/xui/en-us/" under Linux and "%appdata%\SecondLife\skins\default\xui\en-us\" under Windows) containing the following XUI floater definition:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<floater name="lua_floater_test" title="test floater"
 can_close="true" can_drag_on_left="false" can_minimize="true" can_resize="true"
 width="284" height="570" min_width="284" min_height="570">
   <text_editor name="textedit1" font="SansSerif" word_wrap="true"
    left="12" bottom="-86" width="260" height="64" follows="left|top|right" />
   <line_editor name="lineedit1" font="SansSerif" follows="left|top|right"
    tool_tip="Give the path or UUID of the folder you want to set as the root folder for the inventory panel, then press the 'Set folder' button."
    left="12" bottom_delta="-22" width="260" height="18" />
   <inventory_panel name="inventory1" allow_multi_select="true" border="true"
     left="12" bottom_delta="-166" width="260" height="164" follows="left|top|right" />
   <scroll_list name="list1" multi_select="true"
    background_visible="true" draw_border="true" draw_stripes="true" draw_heading="true"
    left="12" bottom_delta="-88" width="260" height="86" follows="left|top|right">
      <column name="col0" label="Name" dynamicwidth="true" />
      <column name="col1" label="Date" width="120" />
   <name_list name="namelist1" multi_select="true"
    background_visible="true" draw_border="true" draw_stripes="true" draw_heading="true"
    left="12" bottom_delta="-88" width="260" height="86" follows="left|top|right|bottom">
      <column name="name" label="Name" dynamicwidth="true" />
   <spinner name="spin1" label="Amount" font="SansSerif" label_width="64"
    decimal_digits="0" increment="1" min_val="0" max_val="255"
    left="12" bottom_delta="-28" width="120" height="24" follows="left|bottom" />
   <check_box name="check1" initial_value="false"
    label="Confirm" font="SansSerifSmall"
    left_delta="140" bottom_delta="6" width="120" height="20" follows="left|bottom" />
   <slider name="slider1" can_edit_text="false" min_val="0" max_val="1.0" increment="0.1" decimal_digits="1"
    show_text="true" label="Magnitude"
    left="12" bottom_delta="-20" height="16" width="260" follows="left|bottom"/>
   <radio_group name="radio1" draw_border="false"
    left="12" bottom_delta="-20" width="260" height="16" follows="left|bottom">
      <radio_item name="0" value="0"
       left="0" bottom="-16" width="40" height="16" follows="left|bottom">
         1st choice
      <radio_item name="1" value="1"
       left_delta="42" bottom_delta="0" width="40" height="16" follows="left|bottom">
         2nd choice
      <radio_item name="2" value="2"
       left_delta="42" bottom_delta="0" width="40" height="16" follows="left|bottom">
         3rd choice
   <combo_box name="combo1" allow_text_entry="false" max_chars="20"
    left="12" bottom_delta="-24" width="120" height="18" follows="left|bottom">
      <combo_item name="1st" type="string" length="1" value="1">
         1st combo item
      <combo_item name="2nd" type="string" length="1" value="2">
         2nd combo item
   <flyout_button name="flyout1" label="Send text" font="SansSerif"
    height="20" width="120" bottom_delta="0" left_delta="130" follows="left|bottom">
      <flyout_button_item value="send_text" name="send_text">
         Send text
      <flyout_button_item value="send_line" name="send_line">
         Send line
   <button name="button1" label="Set folder" font="SansSerif"
    tool_tip="Attempts to set the root folder of the inventory panel with the path or UUID of the folder entered in the input line."
    left="12" bottom_delta="-26" width="80" height="20" follows="bottom|right" />
   <button name="button2" label="Cancel" font="SansSerif"
    left_delta="90" bottom_delta="0" width="80" height="20" follows="bottom|right" />
   <button name="button3" label="OK" font="SansSerif"
    left_delta="90" bottom_delta="0" width="80" height="20" follows="bottom|right" />

Then, add this code to your automation.lua script:
function OnLuaFloaterOpen(name, parameter)
   print("Opened floater: " .. name .. " with parameter: " .. parameter)
   if name == "test" then
      SetLuaFloaterCommand("test","spin1","print('Spinner=' .. GetValue())")
      SetLuaFloaterCommand("test","check1","print('Check=' .. GetValue())")
      SetLuaFloaterCommand("test","combo1","print('Combo=' .. GetValue())")
      SetLuaFloaterCommand("test","radio1","print('Radio=' .. GetValue())")
      SetLuaFloaterCommand("test","flyout1","if GetValue() == 'send_line' then;print('Input line=' .. GetLuaFloaterValue(GetFloaterName(),'lineedit1'));else;print('Text=' .. GetLuaFloaterValue(GetFloaterName(),'textedit1'));end")
      SetLuaFloaterCommand("test","button3","print('Magnitude=' .. GetLuaFloaterValue(GetFloaterName(),'slider1'));FloaterClose()")
      SetLuaFloaterValue("test","list1","<BOLD>Item 1|<1.0,0.5,0.25>2019-01-08")
      SetLuaFloaterValue("test","list1","<ITALIC>Item 2|<red2>2019-01-09")
      SetLuaFloaterValue("test","list1","<BOLD><ITALIC><blue2>Item 3|2019-01-01")
      SetLuaFloaterValue("test","list1","<green3>Item 4|<ITALIC>2019-01-11")

function OnLuaFloaterClose(name, parameter)
   print("Closed floater: " .. name .. " - Parameter: " .. parameter)

function OnLuaFloaterAction(name, control, value)
   print("Floater: " .. name .. " - Control: " .. control .. " - Value: " .. value)
   if value ~= "" and (string.find(control, "list") or string.find(control, "inventory")) then
      local values = GetLuaFloaterValues(name, control)
      local i
      for i = 1, #values, 1 do
         print("  --> value #" .. i .. " = " .. values[i])

Finally, log in with the viewer and type in the chat:
/lua OpenLuaFloater("test")
to test the result.

Have fun ! 8-)

2019-01-05 11:49:39
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
The Cool VL Viewer v1.26.22.36 released today implements Lua script files preprocessing support via a built-in prepocessor.

When loading a Lua script file (be it the automation script or any script you would load via the "Advanced" -> "Lua scripting" menu), the viewer first attempts to load the file as a genuine Lua script file and, on failure (which happens if it encounters preprocessor #directives elsewhere than on the first line of the script (*)), it hands over the file to the built-in preprocessor, which returns a (long) string containing the preprocessed sources, the Lua interpreter then being fed again with it in a second (and last) attempt to execute the script.

(*) As per Lua specifications, the first line of any script that would start with a '#' is ignored. This is to cope with shebang lines.

2019-02-23 14:40:41
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
The Cool VL Viewer v1.28.0.6 released today implements Lua automation threads.

So far, the Cool VL Viewer could only execute (relatively) short sequences of Lua code based on automation script callbacks (or command lines), and that Lua code was executed within the main thread/loop of the viewer, meaning the more complex the code and the more the frame rate would 'hiccup' (given the burst-like load of such Lua callbacks). Also, to avoid freezes or infinite loops, the Lua code executed by the automation script is subject to a watchdog (short) timeout, meaning you cannot perform very complex and long processing this way.

With automation threads, you may now run a full Lua program (and in fact up to 8 Lua programs) in the background (your OS will affect those threads to unused or lightly loaded CPU cores, meaning they won't even slow down the viewer itself !).

A Lua thread is started (from the automation script only) by loading a separate Lua file and executing it as a separate "Lua state". However, that thread still gets the opportunity to call the viewer-specific Lua commands (even non-thread-safe ones, thanks to a special and transparent mechanism), just like if they would be executed by the automation script itself. It may also exchange data with the automation script (and other threads) via "signals".

A Lua thread source is a Lua program file containing at least a ThreadRun() function as an entry point for the thread looping code. ThreadRun() is called at each thread loop and must return a boolean; when the latter is true, the thread keeps running and ThreadRun() is called again after a 1ms "sleep" (which is used both to yield to the OS and allow threads rescheduling by the latter, and to avoid "eating up" a full CPU core when ThreadRun() executes very short sequences of Lua code). Whenever ThreadRun() returns false, the thread is stopped and destroyed.
When it is launched by the automation script (via the StartThread() function), the thread may receive parameters in a global "argv" table.

Note that to avoid infinite loops and to allow timely detection of thread stopping requests from the viewer, the ThreadRun() function execution time is still bound by a 0.5s watchdog. However, a special Sleep() Lua function is available to threads, which resets the watchdog when invoked (because signals and thread stopping requests are checked/processed during the Sleep() call, even when sleeping for 0ms); so, even if each ThreadRun() involves complex/long processing, you can ensure it will not be interrupted by the watchdog under normal operation, by calling Sleep() appropriately.

The Lua thread program file may also contain an OnSignal() callback (and the automation script may also have such a callback to receive data from threads). This callback is entered whenever another thread (or the automation script) invokes SendSignal(), directing it to our thread.

As for the viewer-specific Lua functions which are not thread-safe (e.g. SendChat()), the mechanism I implemented ensures they can be called from threads nonetheless (they cause the thread to pause and set a variable indicating to the viewer main thread that it needs the corresponding code to be ran on its behalf, which is performed during the (badly named) "idle loop" of the viewer).

The print() and warn() Lua functions receive a special treatment: they use an internal print buffer when invoked from a Lua thread and the text gets printed in the viewer chat on the next return of ThreadRun(), on the next invocation of Sleep() or of a non-thread-safe viewer-specific Lua function invocation (whichever happens first).

Here is a (dummy) example of how to use threads:

1.- Add this code to your automation.lua script:
function OnAutomationRequest(request)
    -- Starts a new thread
    if request == "thread" then
        local argv = { "foo", "bar", name="My thread" }
        local thead_id = StartThread("thread.lua", argv)
        if thead_id then
            return "Thread started with Id: " .. string.format("%d", thead_id)
            return "Failure to start a new thread"
    -- Stops a thread by Id
    local i, j = string.find(request, "-thread ")
    if i == 1 then
        local thead_id = tonumber(string.sub(request, j + 1))
        if StopThread(thead_id) then
            return "Stopped"
            return "No such thread"
    -- Checks for thread existence, by Id
    local i, j = string.find(request, "thread%? ")
    if i == 1 then
        local thead_id = tonumber(string.sub(request, j + 1))
        return tostring(HasThread(thead_id))
    -- Sends a signal to a thread, by Id
    local i, j = string.find(request, "thread! ")
    if i == 1 then
        local thead_id = tonumber(string.sub(request, j + 1))
        return tostring(SendSignal(thead_id, { "signal" }))
    return ""

function OnSignal(from_id, timestamp, sig)
    print("Signal received from thread Id: " .. string.format("%d", from_id) .. " - Timestamp: " .. tostring(timestamp) .. "s. Contents:")
    for k, v in pairs(sig) do
        print(tostring(k) .. ": " .. tostring(v))

2.- Create a "thread.lua" file and place it in user_settings/include/ (or in user_settings):
end_time = 0

function ThreadRun()
    local now = GetFrameTimeSeconds()
    if end_time == 0 then
        end_time = now + 30
        print("Thread started. Now: " .. tostring(now) .. "s")
        if GetThreadID() ~= 1 then
            print("Will exit after: " .. tostring(end_time) .. "s")
        if argv then
            print("Arguments passed:")
            for k, v in pairs(argv) do
                print(tostring(k) .. ": " .. tostring(v))
        local agent_info = GetAgentInfo()
        print("My avatar name / display name: " .. GetAvatarName(agent_info["id"]) .. " / " .. agent_info["display_name"])
        if HasThread(1) and GetThreadID() ~= 1 then
            print("Pinging thread 1: " .. tostring(SendSignal(1, { "ping" })))
    if GetThreadID() ~= 1 and now > end_time then
        print("Stopping thread. Time: " .. tostring(now) .. "s")
        return false
    return true

function OnSignal(from_id, timestamp, sig)
    print("Signal received from thread Id: " .. string.format("%d", from_id) .. " - Timestamp: " .. tostring(timestamp) .. "s. Contents:")
    for k, v in pairs(sig) do
        print(tostring(k) .. ": " .. tostring(v))
    print("Reported to automation script: " .. tostring(SendSignal(0, { "got it" })))

3.- Start the viewer, and from the chat, enter:
/lua print(AutomationRequest("thread"))
/lua print(AutomationRequest("thread"))
/lua print(AutomationRequest("thread! 2"))
/lua print(AutomationRequest("thread"))
/lua print(AutomationRequest("thread? 1"))
/lua print(AutomationRequest("-thread 1"))
/lua print(AutomationRequest("thread? 1"))

2020-08-22 09:22:48
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 4937
This is just an example of what can be done when using LSL and Lua in conjunction (here to automatically invite residents to a group based on a list of names stored in a note card). Do not forget to enable "Advanced" -> "Lua scripting" -> "Accept Lua from LSL scripts". To work, this scripts needs to be executed while logged in with the Cool VL Viewer is v1.28.2.52 or newer (it needs the new AgentGroupInvite() Lua function).

// Cool Group Inviter script v1.00 (c)2021 Henri Beauchamp.
// Released under the GPL license:

// The UUID of the group you want to invite to.
// (this is an invalid group Id in this example).
string GroupId = "e13c93e1-b51e-d0c2-e889-6cf6e3015ead";
// The UUID of the role in the group (keep empty for the default "Member" role).
string RoleId = "";
// The name of the notecard (which must be placed along this script in the
// inviter object) containing the names of the avatars to invite: one per line.
string Notecard = "*Invited*";
// The Lua prefix configured in the Cool VL Viewer "LuaScriptCommandPrefix"
// debug setting.
string LuaPrefix = "/lua ";

key NotecardQueryId;
key AvatarQueryId;
integer Counter;
integer Channel;
integer Handle;
integer AvatarIndex;
list Invited;

SetHoverText(string msg) {
    llSetText(msg, <0.0, 1.0, 1.0>, 1.0);

// This function asks the viewer to perform via Lua a group data request and
// sends the reply to our private 'Channel'. The "roles_list_ok" flag in the
// group data Lua table is either 'nil' (when the agent groups have not yet
// been received, or when the group does not exist, or when the agent is not a
// member of this group), 'false' (roles data not yet fully received) or 'true'
// (all data received by the viewer, and we are therefore ready for invites).
CheckGroupData() {
    string cmd = LuaPrefix + "t=GetAgentGroupData(\"" + GroupId;
    cmd += "\");SendChat(\"/" + (string)Channel + "roles_list_ok = \"";
    cmd += " .. tostring(t[\"roles_list_ok\"]))";

default {
    state_entry() {
        if (llGetInventoryType(Notecard) == INVENTORY_NOTECARD) {
            NotecardQueryId = AvatarQueryId = NULL_KEY;
            Channel = 30000 - (integer)llFrand(10000.0);
            SetHoverText("Group inviter ready. Touch to start.");
        } else {
            // Leave NotecardQueryId and AvatarQueryId as empty strings
            SetHoverText("Missing \"" + Notecard + "\" notecard.");

    on_rez(integer param) {

    changed(integer change) {
        if (change & CHANGED_INVENTORY) {

    listen(integer chan, string name, key id, string msg) {
        if (msg == "roles_list_ok = nil") {
            if (Counter++ == 0) {
                // Wait for the viewer to receive the agent groups list and
                // check again...
            SetHoverText("No such group or not a member of this group !");
            Handle = 0;
        } else if (msg == "roles_list_ok = false") {
            // Wait a bit for the viewer to receive all data and check again...
        } else if (msg == "roles_list_ok = true") {
            SetHoverText("Reading invitations list...");       
            Invited = [];
            Counter = 0;
            NotecardQueryId = llGetNotecardLine(Notecard, Counter);
            Handle = 0;

    touch_start(integer n) {
        if (llDetectedKey(0) != llGetOwner() || Handle != 0 ||
            NotecardQueryId != NULL_KEY || AvatarQueryId != NULL_KEY) {
            // Either the touch was not from our owner or work is in progress,
            // or the notecard is absent (Ids are then empty strings).
        SetHoverText("Checking for group data availability...");
        // Open our channel to use for sending Lua commands replies back to us
        Handle = llListen(Channel, "", llGetOwner(), "");
        Counter = 0;    // We need to check at least twice...

    dataserver(key query_id, string data) {
        if (query_id == NotecardQueryId) {
            if (data == EOF) {
                NotecardQueryId = NULL_KEY;
                Counter = llGetListLength(Invited);
                if (Counter > 0) {
                    AvatarIndex = 0;
                    AvatarQueryId = llRequestUserKey(llList2String(Invited,
            } else {
                data = llStringTrim(data, STRING_TRIM);
                if (llStringLength(data) > 2) {
                    SetHoverText("Adding:\n" + data);
                    Invited += data;
                NotecardQueryId = llGetNotecardLine(Notecard, ++Counter);
        } else if (query_id == AvatarQueryId) {
            if ((key)data != NULL_KEY) {
                string tmp = "Invitation " + (string)(AvatarIndex + 1) + "/";
                tmp += (string)Counter + ":\n";
                tmp += llList2String(Invited, AvatarIndex);
                tmp = LuaPrefix + "AgentGroupInvite(\"" + data;
                tmp += "\", \"" + GroupId;
                if (RoleId != "") {
                    tmp += "\", \"" + RoleId;
                llOwnerSay(tmp + "\")");
            if (++AvatarIndex < Counter) {
                llSleep(0.55);   // Requests throttled server-side at 1.9 per second
                AvatarQueryId = llRequestUserKey(llList2String(Invited, AvatarIndex));
                SetHoverText("Invitations finished.");
                AvatarQueryId = NULL_KEY;

2021-12-18 09:16:56
Profile WWW
Display posts from previous:  Sort by  
This topic is locked, you cannot edit posts or make further replies.   [ 8 posts ] 

Who is online

Users browsing this forum: No registered users and 2 guests

You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group
Designed by ST Software.