Cool VL Viewer forum

View unanswered posts | View active topics It is currently 2024-03-19 03:07:47



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

Joined: 2009-03-17 18:42:51
Posts: 5523
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 (70+ 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: 5523
Sample/demonstration automation script (user_settings/automation.lua):
Code:
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
shield_on=false
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
    SideBarHideOnRightClick()
    SideBarHide(false)
    SideBarButton(1, "\u{2699}", "OpenFloater('preferences')", "Opens the Preferences floater")
    -- Setup the side bar button for spelling language toggling:
    ToggleSpellCheckLanguage(false)
    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:
    OnAutomationMessage("shield-off")
    -- Setup the side bar button for attachments and layers protection:
    local agent = GetAgentInfo()
    if agent.rlv then
        OnAutomationMessage("unprotect")
    end
    -- 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
    end
    -- Restore last Windlight and water settings, if any
    if account_settings.last_sky then
        ApplySkySettings(account_settings.last_sky)
    end
    if account_settings.last_water then
        ApplyWaterSettings(account_settings.last_water)
    end
    -- Derender objects we never want to see
    for i = 1, #derendered_objects, 1 do
        DerenderObject(derendered_objects[i])
    end
     -- 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
    DefineAvatarPieMenu()
 end

-- 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("")
    else
        StatusBarLuaIcon("SideBarHide(false)", "Shows the Lua buttons side bar")
    end
end

-- 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"
        else
            lang = "fr_fr"
        end
        SetDebugSetting("SpellCheckLanguage", lang)
    end
    if lang == "fr_fr" then
        SideBarButton(2, "En", "AutomationMessage('language')", "Toggles the spell checking language to English (US)")
    else
        SideBarButton(2, "Fr", "AutomationMessage('language')", "Toggles the spell checking language to French")
    end
end

-- This callback receives the messages sent by the various buttons we setup via
-- the AutomationMessage() function.
function OnAutomationMessage(text)
    if text == "shield-on" then
        shield_on=true
        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
        shield_on=false
        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
        ToggleSpellCheckLanguage(true)
    elseif text == "protect" then
        SideBarButton(8, "9beb8cdd-3dce-53c2-b28e-e1f3bc2ec0a4.tga", "AutomationMessage('unprotect')", "Unlock protected layers and attachments")
        SideBarButtonToggle(8, true)
        LockAttachmentsAndLayers()
        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)
        ExecuteRLV("clear")
        OpenNotification(0, "Protected attachments and layers unlocked")
    end
end

-- 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
        AgentSit()
    end
    OpenNotification(0, "Push detected, magnitude = " .. mag)
end

-- 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
    end
    if text == "/fc" then
        SetDebugSetting("CameraFrontView", not GetDebugSetting("CameraFrontView"))
        return ""
    end
    if text == "/dd" then
        SetDebugSetting("RenderFarClip", 256)
        return ""
    end
    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
        end
        SetDebugSetting("RenderFarClip", distance)
        return ""
    end
    if text == "/z" then
        SetDebugSetting("AvatarOffsetZ", 0)
        return ""
    end
    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
        end
        SetDebugSetting("AvatarOffsetZ", offset)
        return ""
    end
    i, j = string.find(text, "/g ")
    if i == 1 then
        text = "/me smiles softly, \"Greetings" .. string.sub(text, j) .. ".\""
    end
    return text
end

-- 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))
        return
    end
    if known_ids[id] then
        return
    end
    known_ids[id] = true
    if is_avatar then
        OpenNotification(0, name .. " is a new chatting avatar: displaying profile.")
        OpenFloater("avatar info", id)
    else
        OpenNotification(0, name .. " is a new chatting object: inspecting it.")
        OpenFloater("inspect", id)
    end
end

-- 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"
    end
    return ""
end

-- 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)
end

function OnInstantMsg(session_id, origin_id, t, name, text)
    if known_sessions[session_id] then
        return
    end
    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 !")
end

-- 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)
            return
        end
    end
    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
    end
    SetDebugSetting("RenderFarClip", dd)
end

-- 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')")
            else
                MakeDialog("Retry TP",
                           "To auto-retry the TP, give the max allowed number of agents for this sim, else press the Cancel button",
                           max_agents,
                           "Cancel", "", "OK",
                           "DialogClose()", "", "DialogClose()")
            end
        else
            -- Set the draw distance for the arrival sim, after letting a chance
            -- to the viewer to connect to potential neighbor sims.
            CallbackAfter(8, SetDrawDistance)
        end
        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
    end
end

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

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

-- 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
    end
    if water_settings_name ~= "" then
        account_settings.last_water = water_settings_name
    end
    SetPerAccountData(account_settings)
end

-- 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")
end

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"
    end
    avatars_colors[data.object_id] = color
    SetAvatarMinimapColor(data.object_id, color)
    SetAvatarNameTagColor(data.object_id, color)
end

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

-- 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
                break
            end
        end
    end
    for k, v in pairs(protected) do
        ExecuteRLV("remoutfit:" .. k .. "=n")
    end
    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
                break
            end
        end
    end
    for k, v in pairs(protected) do
        ExecuteRLV("remattach:" .. k .. "=n")
    end
end


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

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

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


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

Joined: 2009-03-17 18:42:51
Posts: 5523
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):
Code:
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
        return
    end
    desc = string.sub(desc, j + 1)
    i, j = string.find(desc, "*/")
    if not i then
        return
    end
    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
        ApplySkySettings(sky)
    end
    if water and water ~= "" then
        ApplyWaterSettings(water)
    end
end

Notes:
  • 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: 5523
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:
Code:
<?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" />
   </scroll_list>
   <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" />
   </name_list>
   <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>
      <radio_item name="1" value="1"
       left_delta="42" bottom_delta="0" width="40" height="16" follows="left|bottom">
         2nd choice
      </radio_item>
      <radio_item name="2" value="2"
       left_delta="42" bottom_delta="0" width="40" height="16" follows="left|bottom">
         3rd choice
      </radio_item>
   </radio_group>
   <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>
      <combo_item name="2nd" type="string" length="1" value="2">
         2nd combo item
      </combo_item>
   </combo_box>
   <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>
      <flyout_button_item value="send_line" name="send_line">
         Send line
      </flyout_button_item>
   </flyout_button>
   <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" />
</floater>


Then, add this code to your automation.lua script:
Code:
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","button1","SetLuaFloaterValue(GetFloaterName(),'inventory1',GetLuaFloaterValue(GetFloaterName(),'lineedit1'))")
      SetLuaFloaterCommand("test","button2","print('Cancel');FloaterClose()")
      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")
      SetLuaFloaterValue("test","namelist1",GetAgentInfo().id)
      SetLuaFloaterValue("test","namelist1","<GROUP>d1cc93e1-b51e-d0c2-e699-6c06e3015e9d")
   end
end

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

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])
      end
   end
end


Finally, log in with the viewer and type in the chat:
Code:
/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: 5523
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: 5523
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:
Code:
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)
        else
            return "Failure to start a new thread"
        end
    end
    -- 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"
        else
            return "No such thread"
        end
    end
    -- 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))
    end
    -- 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" }))
    end
    return ""
end

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))
    end
    print("---------------")
end


2.- Create a "thread.lua" file and place it in user_settings/include/ (or in user_settings):
Code:
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")
        end
        if argv then
            print("Arguments passed:")
            for k, v in pairs(argv) do
                print(tostring(k) .. ": " .. tostring(v))
            end
            print("-----------------")
        end
        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" })))
        end
    end
    if GetThreadID() ~= 1 and now > end_time then
        print("Stopping thread. Time: " .. tostring(now) .. "s")
        return false
    end   
    return true
end

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))
    end
    print("---------------")
    print("Reported to automation script: " .. tostring(SendSignal(0, { "got it" })))
end


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"))
etc...


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

Joined: 2009-03-17 18:42:51
Posts: 5523
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).

Code:
// Cool Group Inviter script v1.00 (c)2021 Henri Beauchamp.
// Released under the GPL license: https://www.gnu.org/licenses/gpl-3.0.en.html

// 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\"]))";
    llOwnerSay(cmd);
}

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) {
        llResetScript();
    }

    changed(integer change) {
        if (change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }

    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...
                llSleep(5.0);
                CheckGroupData();
                return;
            }
            SetHoverText("No such group or not a member of this group !");
            llListenRemove(Handle);
            Handle = 0;
        } else if (msg == "roles_list_ok = false") {
            // Wait a bit for the viewer to receive all data and check again...
            llSleep(10.0);
            CheckGroupData();
        } else if (msg == "roles_list_ok = true") {
            SetHoverText("Reading invitations list...");       
            Invited = [];
            Counter = 0;
            NotecardQueryId = llGetNotecardLine(Notecard, Counter);
            llListenRemove(Handle);
            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).
            return;
        }
        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...
        CheckGroupData();
    }

    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,
                                                                   AvatarIndex));
                }
            } 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);
                SetHoverText(tmp);
                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));
            }
            else
            {
                SetHoverText("Invitations finished.");
                AvatarQueryId = NULL_KEY;
            }
        }
    }
}


2021-12-18 09:16:56
Profile WWW

Joined: 2009-03-17 18:42:51
Posts: 5523
I just updated the PDF manual with functions for side bar buttons which can toggle fps limiting and optionally the auto-maximization of the draw distance based on the frame render time (i.e. when there is enough time left after rendering a frame when compared to the chosen fps limit, the DD is increased, and on the contrary, it is decreased when the fps actual rate falls below that chosen limit). The latter feature is very handy when sailing or flying, since it maximizes your view distance without ruining your frame rate.

I will not update the script above (it is updated in the PDF manual however), but here is what you need to change in that example automation.lua script or your own script:

Code:
-- Side bar buttons assignment
SBB_FPSLIMIT = 6
SBB_FPSAUTO = 8

-- Minimum draw distance for automatic FPS-based adjustments
MIN_DRAW_DISTANCE = 256

-- Global flag, set when auto-adjusting the draw distance
auto_tune_fps = false

function OnLogin(location, moved, autologin)
    .../...
    -- Initialize the side bar buttons
    OnAutomationMessage("fps_unlimit")
    OnAutomationMessage("fps_no_tuning")
    .../...
end

-- These functions are used to limit the FPS rate and adjust the draw distance

function ToggleFPSLimiting(on)
    if on then
        SideBarButton(SBB_FPSLIMIT, "\u{229F}", "AutomationMessage('fps_unlimit')", "Lift the limit on the fps rate.")
        SideBarButtonToggle(SBB_FPSLIMIT, true)
        SetDebugSetting("FrameRateLimit", 60)
    else
        SideBarButton(SBB_FPSLIMIT, "\u{229F}", "AutomationMessage('fps_limit')", "Limit the fps rate.")
        SideBarButtonToggle(SBB_FPSLIMIT, false)
        SetDebugSetting("FrameRateLimit", 0)
    end
end

function OnAveragedFPS(fps, limited, frame_time)
    if not auto_tune_fps then
        return
    end
    -- When below 60fps, we need to reduce the draw distance when possible
    if fps < 60 then
        local dd = GetDebugSetting("RenderFarClip")
        -- Frame rate is way too low, reset to minimum draw distance
        if fps < 30 then
            if dd > MIN_DRAW_DISTANCE then
                SetDebugSetting("RenderFarClip", MIN_DRAW_DISTANCE)
            end
        -- Frame rate below the 60fps target, reduce the draw distance if possible
        elseif dd >= MIN_DRAW_DISTANCE then
            dd = dd - 32
            if dd < MIN_DRAW_DISTANCE then
                dd = MIN_DRAW_DISTANCE
            end
            SetDebugSetting("RenderFarClip", dd)
        end
    -- The actual frame render time is small enough: increase the draw distance if possible
    elseif frame_time < 8.333 then
        local dd = GetDebugSetting("RenderFarClip")
        if dd < 512 then
            dd = dd + 32
            if dd > 512 then
                dd = 512
            end
            SetDebugSetting("RenderFarClip", dd)
        end
     end
end

function OnAutomationMessage(text)
.../....
    -- Used to limit the frame rate to 60 fps
    elseif text == "fps_limit" then
        OnAutomationMessage("fps_no_tuning")
        ToggleFPSLimiting(true)
        OpenNotification(0, "Frame rate limited to 60 fps")
    -- Used to lift the limitation on the frame rate
    elseif text == "fps_unlimit" then
        ToggleFPSLimiting(false)
        OpenNotification(0, "Frame rate now unlimited")
    -- Used to limit the frame rate to 60 fps *and* to auto-adjust the draw distance
    -- to be the largest possible without falling below 60fps.
    elseif text == "fps_tuning" then
        OnAutomationMessage("fps_unlimit");
        SideBarButton(SBB_FPSAUTO, "\u{22A1}", "AutomationMessage('fps_no_tuning')", "Do not tune draw distance based on fps rate.")
        auto_tune_fps = true
        SideBarButtonToggle(SBB_FPSAUTO, true)
        SetDebugSetting("FrameRateLimit", 60)
      OpenNotification(0, "FPS-rate based draw distance tuning turned on")
    -- Used to stop the auto-tuning of the draw distance
    elseif text == "fps_no_tuning" then
        ToggleFPSLimiting(GetDebugSetting("FrameRateLimit"))
        auto_tune_fps = false
        SideBarButtonToggle(SBB_FPSAUTO, false)
        SideBarButton(SBB_FPSAUTO, "\u{22A1}", "AutomationMessage('fps_tuning')", "Tune draw distance based on fps rate.")
        OpenNotification(0, "FPS-rate based draw distance tuning turned off")
    end
end


2024-01-15 15:05:37
Profile WWW
Display posts from previous:  Sort by  
This topic is locked, you cannot edit posts or make further replies.   [ 9 posts ] 

Who is online

Users browsing this forum: No registered users and 1 guest


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:  
cron
Powered by phpBB® Forum Software © phpBB Group
Designed by ST Software.