Difference between revisions of "SLua Alpha"

From Second Life Wiki
Jump to navigation Jump to search
 
(21 intermediate revisions by 3 users not shown)
Line 1: Line 1:
[[File:Luau.png|720px|thumb|right|Luau logo]]
{{Warning|This functionality is in alpha. Instability is to be expected, and there may be very sharp edges. At this point it is expected that Luau can crash regions and perform other types of undesirable behavior.


'''🚨 PLEASE NOTE Memory and performance characteristics, and API specifics may change! Scripts are currently being run in unoptimized form for development purposes.'''}}


{{Warning|This functionality is in alpha. Instability is to be expected, and there may be very sharp edges. At this point it is expected that Luau can crash regions and perform other types of undesirable behavior.}}
= Second Life Lua (SLua) Alpha =


[[File:Luau.png|720px|thumb|right|Luau logo]]


= Second Life Luau Alpha =
We're thrilled to announce the launch of the SLua Alpha for Second Life! This significant update introduces the Lua scripting language, offering creators enhanced performance, improved memory efficiency, and a more versatile scripting environment.


We're thrilled to announce the launch of the Luau Alpha for Second Life! This significant update introduces the Luau scripting language, offering creators enhanced performance, improved memory efficiency, and a more versatile scripting environment.
== What is SLua? ==


== What is Luau? ==
SLua is scripting for Second Life based on Luau, a fast, small, safe, and gradually typed embeddable scripting language derived from Lua. It is designed to be backwards compatible with Lua 5.1, incorporating features from future Lua releases and expanding the feature set with type annotations and a state-of-the-art type inference system. Luau is largely implemented from scratch, with the language runtime being a heavily modified version of the Lua 5.1 runtime, featuring a completely rewritten interpreter and other performance innovations.


Luau is a fast, small, safe, and gradually typed embeddable scripting language derived from Lua. It is designed to be backwards compatible with Lua 5.1, incorporating features from future Lua releases and expanding the feature set with type annotations and a state-of-the-art type inference system. Luau is largely implemented from scratch, with the language runtime being a heavily modified version of the Lua 5.1 runtime, featuring a completely rewritten interpreter and other performance innovations.
== Why Lua? ==


== Why Luau? ==
The decision to integrate Lua into Second Life was driven by its ability to meet all the requirements for a scripting engine within the platform. Lua offers a high-quality scripting experience to creators, addressing many of the limitations present in the current LSL (Linden Scripting Language) environment. Its lightweight nature and performance optimizations make it an ideal choice for enhancing the scripting capabilities in Second Life. For more information on why Lua was chosen, please see the [https://wiki.secondlife.com/wiki/Lua_FAQ Lua FAQ].


The decision to integrate Luau into Second Life was driven by its ability to meet all the requirements for a scripting engine within the platform. Luau offers a high-quality scripting experience to creators, addressing many of the limitations present in the current LSL (Linden Scripting Language) environment. Its lightweight nature and performance optimizations make it an ideal choice for enhancing the scripting capabilities in Second Life. For more information on why Luau was chosen, please see the [https://wiki.secondlife.com/wiki/Lua_FAQ Lua FAQ].
== How to Get Started with SLua ==


== How to Get Started with Luau in Second Life ==
In order to play with SLua, you'll need to download our Lua project viewer, and log onto our [https://lindenlab.freshdesk.com/support/solutions/articles/31000156725-accessing-aditi Aditi beta grid].


In order to play with Luau, you'll need to download our Lua project viewer, and log onto our Aditi beta grid.
* Access the latest build of the SLua-enabled Second Life Viewer from [https://releasenotes.secondlife.com/viewer/7.1.12.13526902562.html here].


* Access the latest build of the Luau-enabled Second Life Viewer from [https://releasenotes.secondlife.com/viewer/7.1.12.13526902562.html here].
Once you've got the new viewer and have logged onto the beta grid, head over to these SLua-enabled regions:


Once you've got the new viewer and have logged onto the beta grid, head over to these Luau-enabled regions:
* [secondlife://Aditi/secondlife/SLua%20Yardang/241/235/27 SLua Yardang]
* Luau Yardang
* [secondlife://Aditi/secondlife/SLua%20Tombolo/241/235/27 SLua Tombolo]
* Luau Tombolo
* [secondlife://Aditi/secondlife/SLua%20Mesa/241/235/27 SLua Mesa]
* Luau Mesa
* [secondlife://Aditi/secondlife/SLua%20Tideland/241/235/27 SLua Tideland]
* Luau Tideland


When editing a script in the new Lua project viewer, you'll notice a new '''Compiler''' drop-down near the save button. This drop-down will allow you to select which compiler will be used, as well as which script runtime will be used (LSO2, Mono, Luau).
When editing a script in the new Lua project viewer, you'll notice a new '''Compiler''' drop-down near the save button. This drop-down will allow you to select which compiler will be used, as well as which script runtime will be used (LSO2, Mono, Luau).
Line 37: Line 38:
* '''LSL: Legacy (LSO2)''' - Scripts written in LSL, to be run on the old LSO2 VM
* '''LSL: Legacy (LSO2)''' - Scripts written in LSL, to be run on the old LSO2 VM
* '''LSL: Mono'''- Scripts written in LSL, to be run on the Mono VM
* '''LSL: Mono'''- Scripts written in LSL, to be run on the Mono VM
* '''Lua''' - Scripts written in Lua, to be run on the Luau VM
* '''Lua''' - Scripts written in Lua, to be run on the SLua VM
* '''LSL/Luau'''- Scripts written in LSL, to be run on the Luau VM
* '''LSL/Luau'''- Scripts written in LSL, to be run on the SLua VM


=== Transitioning from LSL to Luau ===
=== Transitioning from LSL to SLua ===
* '''Function Namespacing:'''
* '''Function Namespacing:'''
** In Luau, Linden Lab functions have been moved under the '''ll''' namespace.
** In SLua, Linden Lab functions have been moved under the '''ll''' namespace.
** For example:
** For example:
*** ''llSay'' becomes ''ll.Say''
*** ''llSay'' becomes ''ll.Say''
*** ''llGetPos'' becomes ''ll.GetPos''
*** ''llGetPos'' becomes ''ll.GetPos''
* '''Constants:'''
* '''Lists'''
** Constants like ''PERMISSION_TAKE_CONTROLS'' and ''PRIM_ROT_LOCAL'' are not predefined in Luau.
** Lua indexes begin from 1, unlike LSL where indexes begin from 0.
** You must specify these constants if you wish to reference them in your scripts.
** Lua uses <code>{}</code> for ''tables'', unlike LSL where <code>[]</code> is used for ''lists''.
** When calling LL functions in SLua, lists often have type-strict requirements, unlike Luau in general.
*** For example, <code>ll.SetPrimitiveParams({PRIM_GLOW, 0, 1})</code> will cause a type-error because <code>0</code> is a Luau <code>number</code> type, instead of the LSL <code>integer</code> type expected by [[llSetPrimitiveParams]]. For cases like this, there are special functions which provide the correct data types.
*** Correct: <code>ll.SetPrimitiveParams({PRIM_GLOW, integer(0), 1})</code>
*** Similar functions exist for: '''integer''', '''uuid''' (key), '''vector''', '''quaternion''' (rotation)


=== Luau Libraries ===
=== SLua Libraries ===
* '''Coroutines:'''
* '''Coroutines:'''
** Luau supports coroutines, allowing for cooperative multitasking within scripts.
** SLua supports coroutines, allowing for cooperative multitasking within scripts.
** Key functions include:
** Key functions include:
*** ''coroutine.create''
*** ''coroutine.create''
Line 59: Line 64:
** Refer to the [https://luau.org/library#coroutine-library coroutine library documentation] for more details.
** Refer to the [https://luau.org/library#coroutine-library coroutine library documentation] for more details.
* '''Bitwise Operations:'''
* '''Bitwise Operations:'''
** Luau includes a ''bit32'' library for bitwise operations, enabling more efficient data manipulation.
** SLua includes a ''bit32'' library for bitwise operations, enabling more efficient data manipulation.
** Refer to the [https://luau.org/library#bit32-library bit32 library documentation] for more details.
** Refer to the [https://luau.org/library#bit32-library bit32 library documentation] for more details.
* '''Standard Library:'''
* '''Standard Library:'''
** Luau comes equipped with a standard library of functions designed to manipulate built-in data types.
** SLua comes equipped with a standard library of functions designed to manipulate built-in data types.
** Explore the [https://luau.org/library Luau Standard Library] for a comprehensive list of available functions.
** Explore the [https://luau.org/library Luau Standard Library] for a comprehensive list of available functions.


== Feedback and Support ==
== Feedback and Support ==


We encourage all creators to explore the new Luau scripting capabilities and provide feedback. Your insights are invaluable in refining and enhancing this feature. For more information and to share your experiences, please refer to our [https://wiki.secondlife.com/wiki/Lua_FAQ Lua FAQ].
We encourage all creators to explore the new scripting capabilities and provide feedback. Your insights are invaluable in refining and enhancing this feature. For more information and to share your experiences, please refer to our [https://wiki.secondlife.com/wiki/Lua_FAQ Lua FAQ].


== Example Scripts ==
== Example Scripts ==


To help you get started, we've assembled some example scripts that demonstrate the capabilities of Luau in Second Life. These scripts cover various functionalities and can serve as a foundation for your own creations. Please feel free to propose changes to these scripts, or modify them to your heart's desire!
To help you get started, we've assembled some example scripts that demonstrate the capabilities of SLua. These scripts cover various functionalities and can serve as a foundation for your own creations. Please feel free to propose changes to these scripts, or modify them to your heart's desire!
 
=== default_script.lua ===
This script is roughly equivalent to the default "new script" that gets created for LSL.
<source lang="lua">
function state_entry()
    ll.Say(0, "Hello, Avatar!")
end
 
-- Called when the object is touched.
function touch_start(total_number)
  ll.Say(0, "Touched.")
end
 
-- Invoke state_entry on startup, since simulator doesn't invoke
-- it like it does in LSL
state_entry()
</source>


=== dialog.lua ===
=== dialog.lua ===
Line 134: Line 156:
</source>
</source>


=== dialog_coroutine.lua ===
=== user_input_coroutine.lua ===
This script demonstrates how one could use coroutines to handle dialog responses, with multi-user support.
This script demonstrates [https://www.lua.org/pil/9.html coroutines] and how they can simplify the overarching logic of a script, enabling us to write the bulk of our multi-event code within a centralized function instead of fragmenting across separate event handlers.
<source lang="lua">
<source lang="lua">
-------------------------
-- Wait for user input mid-function before doing something useful with it.
-- Minimal EventLoop
main = function(toucher)
-------------------------
     local handle = ll.Listen(0, "", toucher, "")
local EventLoop = {
     local event = touch_start  -- save function for later
    -- Coroutine -> eventName it’s waiting for
     touch_start = nil          -- disable touch_start
    _coros = {},
    running = false
}
 
function EventLoop:create_task(func)
    local coro = coroutine.create(func)
    self._coros[coro] = false
    self:_run_coro(coro)
    return coro
end
 
function EventLoop:kill_task(coro)
     self._coros[coro] = nil
    if coroutine.status(coro) ~= "dead" then
        coroutine.close(coro)
    end
end
 
-- Internal helper: resumes a coroutine
function EventLoop:_run_coro(coro, ...)
    if coroutine.status(coro) == "dead" then
        return
    end
 
     local old_running = self.running
     self.running = true
 
    self._coros[coro] = false
    local ok, eventAwaited = coroutine.resume(coro, ...)
    self.running = old_running


     if not ok then
     ll.RegionSayTo(toucher, 0, "Do you want pants or gloves?")
        ll.OwnerSay(`Coroutine error: {eventAwaited}`)
    local clothing = coroutine.yield() -- pause the routine's execution here
        self._coros[coro] = nil
    ll.RegionSayTo(toucher, 0, "For men or women?")
        return
    local gender = coroutine.yield()
     end
    ll.RegionSayTo(toucher, 0, "Favorite color?")
    local color = coroutine.yield()
     ll.RegionSayTo(toucher, 0, "Here's "..color.." "..clothing.." for "..gender)


     -- If still alive, 'eventAwaited' is the next event it wants
     ll.ListenRemove(handle)
     self._coros[coro] = eventAwaited
     touch_start = event -- restore touch_start
end
end


function EventLoop:handle_event(eventName, ...)
function touch_start(total_num)
     ll.OwnerSay(`Handling event {eventName}`)
     local toucher = ll.DetectedKey(0)
     local snapshot = table.clone(self._coros)
     routine = coroutine.create(main)   -- new coroutine
     for coro, waitingFor in pairs(snapshot) do
     coroutine.resume(routine, toucher) -- run coroutine (with one argument)
        if coroutine.status(coro) == "dead" then
            self._coros[coro] = nil
        elseif waitingFor == eventName then
            ll.OwnerSay(`Dispatching event {eventName} to {coro}`)
            self:_run_coro(coro, ...)
        end
    end
end
end


-- Coroutines use this to yield until an event
-- When the coroutine is suspended, incoming events can be handled
local function await_event(name)
-- and we can resume() execution of the routine
    assert(EventLoop.running, "await_event called outside a coroutine!")
-- and pass any number of arguments to be returned by yield()
     return coroutine.yield(name)
function listen(channel, name, id, message)
     coroutine.resume(routine, message)
end
end
</source>


-------------------------
=== multi_user_input_coroutine.lua ===
-- Script Logic
Following from the above example, how can we handle multiple users? This is where coroutines shine.
-------------------------
local buttons = {"-", "Red", "Green", "Yellow"}
local dialogInfo = "\nPlease make a choice."


-- Use the chat listener to feed the event-loop
Instead of disabling touches to prevent others from interacting with the object, we can create new copies of the coroutine each time an avatar touches the object. We can then resume whichever coroutine is needed, based on the avatar, while all of them track their own progress separately and automagically.
function listen(channel, name, sender_id, message)
    -- We handle all 'listen' events via the event-loop
    EventLoop:handle_event(`listen_{channel}`, channel, name, sender_id, message)
end
 
-- Called when the script starts
function state_entry()
    -- Seed math.random so each new script run doesn’t repeat the same channels
    math.randomseed(ll.GetUnixTime())
    ll.OwnerSay("Script started with random channels for each user.")
end
 
-- A coroutine function for a single user's dialog flow
local function handle_dialog_for_user(userId)
    -- Use a random channel
    local channel = math.random(0x1, 0xFFFF)
    -- Create a listener for that channel
    local listenHandle = ll.Listen(channel, "", "", "")
 
    while true do
        -- Show the user a dialog
        ll.Dialog(userId, dialogInfo, buttons, channel)
 
        -- Wait for the next 'listen' event (channel, name, sender_id, message)
        local c, n, sid, msg = await_event(`listen_{channel}`)
 
        -- If this "listen" event isn't for our channel/user, ignore it
        if c == channel and sid == userId then
            -- If user pressed "-", re-display the menu, else they picked a final color
            if msg ~= "-" then
                ll.Say(0, `{n} selected {msg}`)
                -- Now that they've chosen something else, remove the listener and finish
                ll.ListenRemove(listenHandle)
                return
            end
        end
    end
end
 
-- Called when the object is touched
function touch_start(num_detected)
    for i=0, num_detected-1 do
        local toucherId = ll.DetectedKey(i)
        -- Create a separate coroutine for each person who touches
        EventLoop:create_task(function()
            handle_dialog_for_user(toucherId)
        end)
    end
end
 
-- Run state_entry on load:
state_entry()
</source>
 
=== vehicle.lua ===
This script demonstrates how one may create their own vehicle scripts, which don’t rely on the underlying LL vehicle system.  
<source lang="lua">
<source lang="lua">
-- Control and permission constants
-- Key: avatar uuid; Value: coroutine thread
local PERMISSION_TAKE_CONTROLS = 0x4
routines = {}
local CONTROL_FWD      = 0x1
local CONTROL_BACK      = 0x2
local CONTROL_UP        = 0x10
local CONTROL_DOWN      = 0x20
local CONTROL_LEFT      = 0x4
local CONTROL_RIGHT    = 0x8
local PRIM_ROT_LOCAL    = integer(29)
 
-- These two will hold the link numbers for left and right front wheels
local LINK_WHEEL_LEFT = 0
local LINK_WHEEL_RIGHT = 0
 
-- Integra Type R Engine properties
local Engine = {
    rpm = 800,              -- current engine RPM (starts at idle)
    gear = 0,              -- 0 = neutral; -1 = reverse; 1-5 for forward gears
    idleRPM = 800,          -- idle RPM
    maxRPM = 8300,          -- redline
    throttle = 0.0,        -- throttle value (0.0 to 1.0)
    gearRatios = {         -- Integra Type R gear ratios
        [-1] = 3.727,      -- reverse gear ratio
        [0]  = 0,          -- neutral
        [1]  = 3.727,
        [2]  = 2.256,
        [3]  = 1.729,
        [4]  = 1.416,
        [5]  = 1.194
    }
}
 
local FINAL_DRIVE = 4.1  -- Final drive ratio
local WHEEL_RADIUS = 0.3  -- in meters
 
-- Vehicle properties (using realistic Integra Type R mass)
local Vehicle = {
    speed = 0,      -- speed in m/s; negative indicates reverse motion
    mass = 1200,    -- mass in kg
    angle = 0,      -- heading angle in radians
    steering = 0,  -- steering input: -1 (left), 0 (none), 1 (right)
    brake = false  -- flag indicating if brakes are applied
}
 
-- Engine constants (using realistic Integra Type R values)
local MAX_TORQUE = 200  -- in Nm
 
-- Brake force constant (for active braking)
local BRAKE_FORCE = 4000  -- in Newtons
 
-- Define a torque curve function:
local function torqueCurve(rpm)
    local peakRPM = 6000
    if rpm < peakRPM then
        return 0.8 + 0.2 * ((rpm - Engine.idleRPM) / (peakRPM - Engine.idleRPM))
    else
        local factor = 1 - 0.5 * ((rpm - peakRPM) / (Engine.maxRPM - peakRPM))
        return math.max(0.5, factor)
    end
end
 
-- Engine braking force when throttle is zero in gear.
local ENGINE_BRAKE_FORCE = 2000  -- in Newtons
 
-- Friction and drag constants
local G = 9.81            -- gravity (m/s^2)
local C_rr_inGear = 0.015 -- rolling resistance coefficient in gear
local C_rr_neutral = 0.05 -- rolling resistance coefficient in neutral
 
local airDensity = 1.225      -- kg/m^3
local dragCoefficient = 0.3  -- aerodynamic drag coefficient
local frontalArea = 2.2      -- in m^2
 
local function getAerodynamicDrag(speed)
    return 0.5 * airDensity * dragCoefficient * frontalArea * speed * speed
end
 
-- Compute engine RPM from vehicle speed when in gear.
local function computeEngineRPMFromSpeed()
    if Engine.gear == 0 then
        return Engine.rpm  -- In neutral, RPM is managed independently.
    else
        local conversionFactor = Engine.gearRatios[Engine.gear] * FINAL_DRIVE * (60 / (2 * math.pi * WHEEL_RADIUS))
        local rpm = math.abs(Vehicle.speed) * conversionFactor
        if rpm < Engine.idleRPM then rpm = Engine.idleRPM end
        return rpm
    end
end


-- For neutral, update RPM toward a target based on throttle.
main = function(toucher)
local function updateEngineRPMNeutral()
     local handle = ll.Listen(0, "", toucher, "")
     local targetRPM = Engine.idleRPM + (Engine.maxRPM - Engine.idleRPM) * Engine.throttle
    local changeRate = 200  -- RPM per tick
    if Engine.rpm < targetRPM then
        Engine.rpm = math.min(Engine.rpm + changeRate, targetRPM)
    elseif Engine.rpm > targetRPM then
        Engine.rpm = math.max(Engine.rpm - changeRate, targetRPM)
    end
end


-- Calculate the engine force delivered to the wheels.
    ll.RegionSayTo(toucher, 0, "Do you want pants or gloves?")
local function getEngineForce()
    local clothing = coroutine.yield()
     if Engine.gear == 0 then
     ll.RegionSayTo(toucher, 0, "For men or women?")
        return 0  -- In neutral, no drive force is transmitted.
    local gender = coroutine.yield()
     else
     ll.RegionSayTo(toucher, 0, "Favorite color?")
        if Engine.throttle == 0 then
    local color = coroutine.yield()
            -- Apply engine braking when throttle is released.
    ll.RegionSayTo(toucher, 0, "Here's "..color.." "..clothing.." for "..gender)
            return -ENGINE_BRAKE_FORCE
        else
            local torque = Engine.throttle * MAX_TORQUE * torqueCurve(Engine.rpm)
            local force = (torque * Engine.gearRatios[Engine.gear] * FINAL_DRIVE) / WHEEL_RADIUS
            return force
        end
    end
end


-- Starts the engine by setting it to idle RPM.
    ll.ListenRemove(handle)
local function startEngine()
     routines[toucher] = nil -- Remove from collection
     Engine.rpm = Engine.idleRPM
    ll.Say(0, "Engine started. RPM: " .. Engine.rpm)
end
end


-- Shifts the gear if valid.
function touch_start(total_num)
local function shiftGear(newGear)
    local toucher = ll.DetectedKey(0)
     if Engine.gearRatios[newGear] then
     local routine = routines[toucher]
         Engine.gear = newGear
    if not routine then -- New user needs new routine
        ll.Say(0, "Shifted to gear " .. newGear)
         routine = coroutine.create(main)
    else
        routines[toucher] = routine -- Add to collection
         ll.Say(0, "Invalid gear: " .. newGear)
         coroutine.resume(routine, toucher)
     end
     end
end
end


local function findWheels()
function listen(channel, name, id, message)
     local total = ll.GetNumberOfPrims()
     coroutine.resume(routines[id], message) -- Resume a specific coroutine
    for i = 1, total do
        local name = ll.GetLinkName(i)
        if name == "WHEEL_LF" then
            LINK_WHEEL_LEFT = i
        elseif name == "WHEEL_RF" then
            LINK_WHEEL_RIGHT = i
        end
    end
end
end
local castRayCounter = 0
local lastGrounded = false
local function isVehicleOnGround()
    castRayCounter = castRayCounter + 1
    if castRayCounter >= 5 then
        castRayCounter = 0
        local pos = ll.GetPos()
        local rayDistance = 4.5  -- threshold distance (in meters) to consider as "grounded"
        local rayStart = pos
        local rayEnd = pos + vector(0, 0, -rayDistance)
        local hitResult = ll.CastRay(rayStart, rayEnd, {})
        if table.getn(hitResult) == 3 then
            lastGrounded = true
        else
            lastGrounded = false
        end
    end
    return lastGrounded
end
-- Timer event: updates engine, vehicle physics, steering, and rotates the wheels based on steering.
function timer()
    local dt = 0.1  -- time step in seconds
    -- For neutral, update RPM based on throttle.
    if Engine.gear == 0 then
        updateEngineRPMNeutral()
    end
    -- Calculate forces.
    local C_rr = (Engine.gear == 0) and C_rr_neutral or C_rr_inGear
    local F_roll = Vehicle.mass * G * C_rr
    local engineForce = getEngineForce()
    local dragForce = getAerodynamicDrag(math.abs(Vehicle.speed))
    local netForce = engineForce - (F_roll + dragForce)
    -- Apply braking force if brakes are engaged.
    if Vehicle.brake then
        netForce = netForce - BRAKE_FORCE
    end
    local acceleration = netForce / Vehicle.mass
    local vel = ll.GetVel()
    local horizontal_vec = vector(vel.x, vel.y, 0)
    Vehicle.speed = (ll.VecMag(horizontal_vec) + acceleration * dt)
    -- Limit vehicle speed based on gear.
    if Engine.gear ~= 0 then
        if Vehicle.speed < 0 then
            Vehicle.speed = 0
        end
        local conversionFactor = Engine.gearRatios[Engine.gear] * FINAL_DRIVE * (60 / (2 * math.pi * WHEEL_RADIUS))
        local maxSpeed = Engine.maxRPM / conversionFactor
        if Vehicle.speed > maxSpeed then
            Vehicle.speed = maxSpeed
        end
    end
    -- When in gear, update RPM based on the new speed.
    if Engine.gear ~= 0 then
        Engine.rpm = computeEngineRPMFromSpeed()
    end
    -- Steering update using angular velocity and quaternions:
    local STEERING_RATE = math.rad(6)  -- desired steering rate per tick (radians)
    local yawDelta = Vehicle.steering * STEERING_RATE  -- small rotation for this tick
    if yawDelta ~= 0 then
        local deltaQuat = ll.Euler2Rot(vector(0, 0, yawDelta))
        local angularSpeed = yawDelta / dt
        local angularVelocity = vector(0, 0, angularSpeed)
        ll.SetAngularVelocity(angularVelocity, 1)
        Vehicle.angle = Vehicle.angle + yawDelta
    else
        ll.SetAngularVelocity(vector(0, 0, 0), 1)
    end
    -- Display engine RPM and speed (converted to mph).
    local speed_mph = math.abs(Vehicle.speed) * 2.23694
    ll.SetText("RPM: " .. math.floor(Engine.rpm) ..
              " | Speed: " .. string.format("%.1f", speed_mph) ..
              " mph", vector(0,0,0), 1)
    -- Only update velocity if the vehicle is near the ground, and we're in gear.
    if isVehicleOnGround() and Engine.gear ~= 0 then
        local velocityVector
        if Engine.gear > 0 then
            velocityVector = vector(Vehicle.speed, 0, 0)
        else
            -- Reverse gear
            velocityVector = vector(-Vehicle.speed, 0, 0)
        end
        ll.SetVelocity(velocityVector, 1)
    end
    -- Update the wheel rotations to visually match the steering.
    local maxWheelTurn = math.rad(30)  -- maximum wheel turn angle in radians
    local wheelAngle = -Vehicle.steering * maxWheelTurn
    if LINK_WHEEL_LEFT > 0 then
        ll.SetLinkPrimitiveParamsFast(
            LINK_WHEEL_LEFT,
            {
                PRIM_ROT_LOCAL,
                ll.Euler2Rot(vector(-4.71239, -wheelAngle, 0))
            }
        )
    end
    if LINK_WHEEL_RIGHT > 0 then
        ll.SetLinkPrimitiveParamsFast(
            LINK_WHEEL_RIGHT,
            {
                PRIM_ROT_LOCAL,
                ll.Euler2Rot(vector(4.71239, wheelAngle, 0))
            }
        )
    end
end
-- Called when the object is touched: starts engine and simulation.
function touch_start(num)
    ll.Say(0, "Touch detected, requesting controls and starting engine.")
    ll.RequestPermissions(ll.GetOwner(), PERMISSION_TAKE_CONTROLS)
    startEngine()
    findWheels()
    ll.SetTimerEvent(0.1)
end
-- Called when runtime permissions are granted.
function run_time_permissions(perm)
    if bit32.band(perm, PERMISSION_TAKE_CONTROLS) then
        ll.Say(0, "Permissions granted.")
        local controlMask = bit32.bor(CONTROL_FWD, CONTROL_BACK, CONTROL_UP, CONTROL_DOWN, CONTROL_LEFT, CONTROL_RIGHT)
        ll.TakeControls(controlMask, 1, 1)
    end
end
-- Handle control events:
-- β€’ CONTROL_FWD applies throttle.
-- β€’ CONTROL_BACK applies the brakes.
-- β€’ CONTROL_UP and CONTROL_DOWN shift gears.
-- β€’ CONTROL_LEFT and CONTROL_RIGHT steer the vehicle.
function control(avatar_id, level, edge)
    local start = bit32.band(level, edge)           
    local finish = bit32.band(bit32.bnot(level), edge)
    local held = bit32.band(level, bit32.bnot(edge)) 
    local untouched = bit32.bnot(bit32.bor(level, edge))
    if bit32.band(held, CONTROL_BACK) ~= 0 then
        Vehicle.brake = true
        Engine.throttle = 0.0
    elseif bit32.band(held, CONTROL_FWD) ~= 0 then
        Vehicle.brake = false
        Engine.throttle = 1.0
        if Engine.gear == 0 then
            updateEngineRPMNeutral()
        end
    else
        Vehicle.brake = false
        Engine.throttle = 0.0
    end
    if bit32.band(start, CONTROL_UP) ~= 0 then
        shiftGear(Engine.gear + 1)
    end
    if bit32.band(start, CONTROL_DOWN) ~= 0 then
        shiftGear(Engine.gear - 1)
    end
    local steeringInput = 0
    if bit32.band(held, CONTROL_LEFT) ~= 0 then
        steeringInput = steeringInput + 1
    end
    if bit32.band(held, CONTROL_RIGHT) ~= 0 then
        steeringInput = steeringInput - 1
    end
    Vehicle.steering = steeringInput
end
</source>
</source>


=== weather_box.lua ===
=== More Examples ===
This script demonstrates coroutines, as well as working with a JSON-based web API:
<source lang="lua">
local TEXTBOX_CHANNEL = 323242
-- kludge because we don't have LSL constants in here yet
local HTTP_BODY_MAXLENGTH = integer(2)
local PRIM_TEXT = integer(26)
local PRIM_SIZE = integer(7)
 
 
gCityName = ""
gLatLong = nil  -- A table of 2 elems when it's populated
gCityRequestTask = nil
gWeatherRequestTask = nil
 
 
 
-----
-- JSON stuff
-----
 
-- From https://github.com/rxi/json.lua/blob/master/json.lua
 
local json = {}
 
local escape_char_map = {
  [ "\\" ] = "\\",
  [ "\"" ] = "\"",
  [ "\b" ] = "b",
  [ "\f" ] = "f",
  [ "\n" ] = "n",
  [ "\r" ] = "r",
  [ "\t" ] = "t",
}
 
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
  escape_char_map_inv[v] = k
end
 
 
local parse
 
local function create_set(...)
  local res = {}
  for i = 1, select("#", ...) do
    res[ select(i, ...) ] = true
  end
  return res
end
 
local space_chars  = create_set(" ", "\t", "\r", "\n")
local delim_chars  = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals      = create_set("true", "false", "null")
 
local literal_map = {
  [ "true"  ] = true,
  [ "false" ] = false,
  [ "null"  ] = nil,
}
 
 
local function next_char(str, idx, set, negate)
  for i = idx, #str do
    if set[str:sub(i, i)] ~= negate then
      return i
    end
  end
  return #str + 1
end
 
 
local function decode_error(str, idx, msg)
  local line_count = 1
  local col_count = 1
  for i = 1, idx - 1 do
    col_count = col_count + 1
    if str:sub(i, i) == "\n" then
      line_count = line_count + 1
      col_count = 1
    end
  end
  error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
 
 
local function codepoint_to_utf8(n)
  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
  local f = math.floor
  if n <= 0x7f then
    return string.char(n)
  elseif n <= 0x7ff then
    return string.char(f(n / 64) + 192, n % 64 + 128)
  elseif n <= 0xffff then
    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
  elseif n <= 0x10ffff then
    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
                      f(n % 4096 / 64) + 128, n % 64 + 128)
  end
  error( string.format("invalid unicode codepoint '%x'", n) )
end
 
 
local function parse_unicode_escape(s)
  local n1 = tonumber( s:sub(1, 4),  16 )
  local n2 = tonumber( s:sub(7, 10), 16 )
  -- Surrogate pair?
  if n2 then
    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
  else
    return codepoint_to_utf8(n1)
  end
end
 
 
local function parse_string(str, i)
  local res = ""
  local j = i + 1
  local k = j
 
  while j <= #str do
    local x = str:byte(j)
 
    if x < 32 then
      decode_error(str, j, "control character in string")
 
    elseif x == 92 then -- `\`: Escape
      res = res .. str:sub(k, j - 1)
      j = j + 1
      local c = str:sub(j, j)
      if c == "u" then
        local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
                or str:match("^%x%x%x%x", j + 1)
                or decode_error(str, j - 1, "invalid unicode escape in string")
        res = res .. parse_unicode_escape(hex)
        j = j + #hex
      else
        if not escape_chars[c] then
          decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
        end
        res = res .. escape_char_map_inv[c]
      end
      k = j + 1
 
    elseif x == 34 then -- `"`: End of string
      res = res .. str:sub(k, j - 1)
      return res, j + 1
    end
 
    j = j + 1
  end
 
  decode_error(str, i, "expected closing quote for string")
end
 
 
local function parse_number(str, i)
  local x = next_char(str, i, delim_chars)
  local s = str:sub(i, x - 1)
  local n = tonumber(s)
  if not n then
    decode_error(str, i, "invalid number '" .. s .. "'")
  end
  return n, x
end
 
 
local function parse_literal(str, i)
  local x = next_char(str, i, delim_chars)
  local word = str:sub(i, x - 1)
  if not literals[word] then
    decode_error(str, i, "invalid literal '" .. word .. "'")
  end
  return literal_map[word], x
end
 
 
local function parse_array(str, i)
  local res = {}
  local n = 1
  i = i + 1
  while 1 do
    local x
    i = next_char(str, i, space_chars, true)
    -- Empty / end of array?
    if str:sub(i, i) == "]" then
      i = i + 1
      break
    end
    -- Read token
    x, i = parse(str, i)
    res[n] = x
    n = n + 1
    -- Next token
    i = next_char(str, i, space_chars, true)
    local chr = str:sub(i, i)
    i = i + 1
    if chr == "]" then break end
    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
  end
  return res, i
end
 
 
local function parse_object(str, i)
  local res = {}
  i = i + 1
  while 1 do
    local key, val
    i = next_char(str, i, space_chars, true)
    -- Empty / end of object?
    if str:sub(i, i) == "}" then
      i = i + 1
      break
    end
    -- Read key
    if str:sub(i, i) ~= '"' then
      decode_error(str, i, "expected string for key")
    end
    key, i = parse(str, i)
    -- Read ':' delimiter
    i = next_char(str, i, space_chars, true)
    if str:sub(i, i) ~= ":" then
      decode_error(str, i, "expected ':' after key")
    end
    i = next_char(str, i + 1, space_chars, true)
    -- Read value
    val, i = parse(str, i)
    -- Set
    res[key] = val
    -- Next token
    i = next_char(str, i, space_chars, true)
    local chr = str:sub(i, i)
    i = i + 1
    if chr == "}" then break end
    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
  end
  return res, i
end
 
 
local char_func_map = {
  [ '"' ] = parse_string,
  [ "0" ] = parse_number,
  [ "1" ] = parse_number,
  [ "2" ] = parse_number,
  [ "3" ] = parse_number,
  [ "4" ] = parse_number,
  [ "5" ] = parse_number,
  [ "6" ] = parse_number,
  [ "7" ] = parse_number,
  [ "8" ] = parse_number,
  [ "9" ] = parse_number,
  [ "-" ] = parse_number,
  [ "t" ] = parse_literal,
  [ "f" ] = parse_literal,
  [ "n" ] = parse_literal,
  [ "[" ] = parse_array,
  [ "{" ] = parse_object,
}
 
 
parse = function(str, idx)
  local chr = str:sub(idx, idx)
  local f = char_func_map[chr]
  if f then
    return f(str, idx)
  end
  decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
 
 
function json.decode(str)
  if type(str) ~= "string" then
    error("expected argument of type string, got " .. type(str))
  end
  local res, idx = parse(str, next_char(str, 1, space_chars, true))
  idx = next_char(str, idx, space_chars, true)
  if idx <= #str then
    decode_error(str, idx, "trailing garbage")
  end
  return res
end
 
 
-----
-- Event Loop
-----
 
-- A naive little event loop that lets you track running coroutines and dispatches
-- the events they're waiting for to them.
 
local EventLoop = {
    -- Coroutine -> awaited event
    _coros = {},
    running = false
}
 
function EventLoop:create_task(func)
    local coro = coroutine.create(func)
    -- false has no semantic meaning here, we just want to reserve the space for
    -- the coroutine in the table without saying we're awaiting anything yet.
    self._coros[coro] = false
    self:_run_coro(coro)
    return coro
end
 
function EventLoop:kill_task(coro)
    self._coros[coro] = nil
    if coroutine.status ~= "dead" then
        coroutine.close(coro)
    end
end
 
-- (internal) run the event
function EventLoop:_run_coro(coro, ...)
    assert(coroutine.status(coro) ~= "dead")
    -- assert(not self.running)
    local old_running = self.running
    self.running = true
 
    -- Since we're running, this isn't currently awaiting anything
    self._coros[coro] = false
    local success, ret = coroutine.resume(coro, ...)
    self.running = old_running
 
    -- If we're not done then ret should be the kind of event we're awaiting.
    -- Otherwise it's an error message or something. Who cares.
    if not success then
        -- Get rid of the coro
        if ret then
            ll.OwnerSay(`Might have an error: {ret}`)
        end
        ret = nil
    end
    self._coros[coro] = ret
end
 
-- Wake up any tasks that are waiting for this kind of event
function EventLoop:handle_event(name, ...)
    -- We may mutate self._coros, so do a clone
    local coros = table.clone(self._coros)
    ll.OwnerSay(`Handling {name}`)
 
    for coro, awaited_event in coros do
        if coroutine.status(coro) == "dead" then
            -- Hmm, this coroutine is dead, prune it.
            self._coros[coro] = nil
        end
 
        if awaited_event == name then
            ll.OwnerSay(`Dispatching {name} to {coro}`)
            self:_run_coro(coro, ...)
        end
    end
end
 
function EventLoop:create_event_catcher(name)
    local function catcher(...)
        ll.OwnerSay("yay")
        EventLoop:handle_event(name, ...)
    end
    return catcher
end
 
 
 
-- Helper function for waiting and telling the EventLoop what event you want
local function await_event(kind)
    -- You had better do this inside a running event loop
    assert(EventLoop.running)
    -- This will yield, the EventLoop will resume us once it has
    -- an event we might be interested in.
    return coroutine.yield(kind)
end
 
 
 
-----
-- Script-specific functions
-----
 
local function hide_children()
    for i=2,ll.GetNumberOfPrims() do
        -- Hide all the child prims
        ll.SetLinkAlpha(i, 0, -1)
        ll.SetLinkPrimitiveParamsFast(i, {
            PRIM_SIZE, vector(0.5, 0.5, 0.5),
            PRIM_TEXT, "", vector(1,1,1), 1
        })
    end
end
 
local function send_textbox(avatar_id, message, channel)
    ll.TextBox(avatar_id, message, channel)
 
    while true do
        local resp_channel, name, id, resp = await_event("listen")
        if id ~= avatar_id or channel ~= resp_channel then
            -- If this isn't a message we're interested in then wait for the next event
            ll.OwnerSay(`{id}, {avatar_id}, {resp_channel}, {channel}`)
            continue
        end
        return resp
    end
end
 
local function make_json_request(url)
    local req_id = ll.HTTPRequest(url, {HTTP_BODY_MAXLENGTH, integer(16000)}, "")
    while true do
        local ev_id, status, metadata, body = await_event("http_response")
        if ev_id ~= req_id then
            -- Not a response for the HTTP request we were waiting on, wait for the next event.
            continue
        end
        return status, metadata, body
    end
end
 
local function request_weather()
    -- And you can use a wrapper like this to make an async function callable within both normal and coroutine contexts.
    -- Normally this sort of thing would not be necessary, but here it is since the EventLoop lives in our own code,
    -- and isn't a property of the actual script engine.
    if gWeatherRequestTask then
        EventLoop:kill_task(gWeatherRequestTask)
    end
 
    gWeatherRequestTask = EventLoop:create_task(function()
    ll.OwnerSay("Requesting weather")
        local status, metadata, body = make_json_request(`https://api.open-meteo.com/v1/forecast?latitude={gLatLong[1]}&longitude={gLatLong[2]}&current=temperature_2m&daily=temperature_2m_max`)
        if status ~= 200 then
            ll.OwnerSay("Error requesting weather")
            return
        end
        local parsed = json.decode(body)


        local current = parsed["current"]
* Find more example scripts at [[Luau Example Scripts]]
        ll.OwnerSay(`Temperature is {current["temperature_2m"]}C`)
        ll.SetText(`Temperature in {gCityName} is currently {current["temperature_2m"]}C`, vector(1,1,1), 1)


        -- Show the highs for each day
* [https://roblox.github.io/lua-style-guide/gotchas/ Lua Gotchas, Footguns and Other Hazards]
        local days = parsed["daily"]["temperature_2m_max"]
        for i=1,#days do
            local link_num = i + 1
            -- Higher temps = bigger on Z, negatives are unrepresentable :)
            local z_size = (days[i] / 30) * 2
            ll.SetLinkAlpha(link_num, 1, -1)
            ll.SetLinkPrimitiveParamsFast(link_num, {
                PRIM_SIZE, vector(0.5, 0.5, z_size),
                PRIM_TEXT, `{days[i]}C`, vector(1,1,1), 1
            })
        end
    end)
end
 
local function ask_for_city()
    local resp = send_textbox(ll.GetOwner(), "What city do you want the weather for?", TEXTBOX_CHANNEL)
    if not resp then
        ll.OwnerSay("Got no response for city request.")
        return
    end
 
    ll.OwnerSay(`You want the weather for {resp}.`)
    gLatLong = nil
    gCityName = resp
 
    hide_children()
 
    -- request the lat and long for this city
    local status, metadata, body = make_json_request(`https://nominatim.openstreetmap.org/search.php?city={ll.EscapeURL(resp)}&format=jsonv2`)
    local parsed = json.decode(body)
    if not #parsed then
        ll.OwnerSay(`Got no results for {resp}!`)
        return
    end
 
    local first_result = parsed[1]
    gLatLong = {first_result["lat"], first_result["lon"]}
    ll.OwnerSay(`City Lat: {gLatLong[1]}, Long: {gLatLong[2]}`)
 
    request_weather()
 
    -- request weather again in 3 mins
    ll.SetTimerEvent(180)
end
 
 
-----
-- Event Handlers
-----
 
 
-- If we don't care about conventionally dispatched events, we can just set the event handler to directly send
-- event data to the event loop.
http_response = EventLoop:create_event_catcher("http_response")
listen = EventLoop:create_event_catcher("listen")
 
 
-- More conventional event handlers work too.
function touch_start(num)
    for i=0,num-1 do
        if ll.GetOwner() ~= ll.DetectedKey(i) then
            -- Go to next loop if it wasn't the owner touching us.
            continue
        end
 
        if gCityRequestTask then
            EventLoop:kill_task(gCityRequestTask)
        end
 
        -- Spin up a task to ask the user for their city
        gCityRequestTask = EventLoop:create_task(ask_for_city)
        break
    end
end
 
function timer()
    request_weather()
end
 
 
-- Not strictly necessary, but I like having the main function have its own scope.
-- Rather than just have top-level logic.
local function main()
    ll.Listen(TEXTBOX_CHANNEL, "", "", "")
    ll.SetText("", vector(1,1,1), 1)
    hide_children()
    ll.OwnerSay("Weatherbox operational!")
end
 
main()
</source>

Latest revision as of 15:55, 13 March 2025

Warning!

This functionality is in alpha. Instability is to be expected, and there may be very sharp edges. At this point it is expected that Luau can crash regions and perform other types of undesirable behavior.

🚨 PLEASE NOTE Memory and performance characteristics, and API specifics may change! Scripts are currently being run in unoptimized form for development purposes.


Second Life Lua (SLua) Alpha

Luau logo

We're thrilled to announce the launch of the SLua Alpha for Second Life! This significant update introduces the Lua scripting language, offering creators enhanced performance, improved memory efficiency, and a more versatile scripting environment.

What is SLua?

SLua is scripting for Second Life based on Luau, a fast, small, safe, and gradually typed embeddable scripting language derived from Lua. It is designed to be backwards compatible with Lua 5.1, incorporating features from future Lua releases and expanding the feature set with type annotations and a state-of-the-art type inference system. Luau is largely implemented from scratch, with the language runtime being a heavily modified version of the Lua 5.1 runtime, featuring a completely rewritten interpreter and other performance innovations.

Why Lua?

The decision to integrate Lua into Second Life was driven by its ability to meet all the requirements for a scripting engine within the platform. Lua offers a high-quality scripting experience to creators, addressing many of the limitations present in the current LSL (Linden Scripting Language) environment. Its lightweight nature and performance optimizations make it an ideal choice for enhancing the scripting capabilities in Second Life. For more information on why Lua was chosen, please see the Lua FAQ.

How to Get Started with SLua

In order to play with SLua, you'll need to download our Lua project viewer, and log onto our Aditi beta grid.

  • Access the latest build of the SLua-enabled Second Life Viewer from here.

Once you've got the new viewer and have logged onto the beta grid, head over to these SLua-enabled regions:

When editing a script in the new Lua project viewer, you'll notice a new Compiler drop-down near the save button. This drop-down will allow you to select which compiler will be used, as well as which script runtime will be used (LSO2, Mono, Luau).

Compiler selection dropdown

Compiler drop-down options:

  • LSL: Legacy (LSO2) - Scripts written in LSL, to be run on the old LSO2 VM
  • LSL: Mono- Scripts written in LSL, to be run on the Mono VM
  • Lua - Scripts written in Lua, to be run on the SLua VM
  • LSL/Luau- Scripts written in LSL, to be run on the SLua VM

Transitioning from LSL to SLua

  • Function Namespacing:
    • In SLua, Linden Lab functions have been moved under the ll namespace.
    • For example:
      • llSay becomes ll.Say
      • llGetPos becomes ll.GetPos
  • Lists
    • Lua indexes begin from 1, unlike LSL where indexes begin from 0.
    • Lua uses {} for tables, unlike LSL where [] is used for lists.
    • When calling LL functions in SLua, lists often have type-strict requirements, unlike Luau in general.
      • For example, ll.SetPrimitiveParams({PRIM_GLOW, 0, 1}) will cause a type-error because 0 is a Luau number type, instead of the LSL integer type expected by llSetPrimitiveParams. For cases like this, there are special functions which provide the correct data types.
      • Correct: ll.SetPrimitiveParams({PRIM_GLOW, integer(0), 1})
      • Similar functions exist for: integer, uuid (key), vector, quaternion (rotation)

SLua Libraries

  • Coroutines:
    • SLua supports coroutines, allowing for cooperative multitasking within scripts.
    • Key functions include:
      • coroutine.create
      • coroutine.status
      • coroutine.resume
    • Refer to the coroutine library documentation for more details.
  • Bitwise Operations:
    • SLua includes a bit32 library for bitwise operations, enabling more efficient data manipulation.
    • Refer to the bit32 library documentation for more details.
  • Standard Library:
    • SLua comes equipped with a standard library of functions designed to manipulate built-in data types.
    • Explore the Luau Standard Library for a comprehensive list of available functions.

Feedback and Support

We encourage all creators to explore the new scripting capabilities and provide feedback. Your insights are invaluable in refining and enhancing this feature. For more information and to share your experiences, please refer to our Lua FAQ.

Example Scripts

To help you get started, we've assembled some example scripts that demonstrate the capabilities of SLua. These scripts cover various functionalities and can serve as a foundation for your own creations. Please feel free to propose changes to these scripts, or modify them to your heart's desire!

default_script.lua

This script is roughly equivalent to the default "new script" that gets created for LSL.

function state_entry()
    ll.Say(0, "Hello, Avatar!")
end

-- Called when the object is touched.
function touch_start(total_number)
   ll.Say(0, "Touched.")
end

-- Invoke state_entry on startup, since simulator doesn't invoke 
-- it like it does in LSL
state_entry()

dialog.lua

This script demonstrates how one can interact with dialog menus.

-- Define the menu buttons and dialog message.
local buttons = {"-", "Red", "Green", "Yellow"}
local dialogInfo = "\nPlease make a choice."

local ToucherID = nil
local dialogChannel = nil
local listenHandle = nil

-- This function is called when the script first starts.
function state_entry()
    -- Get the object's key and compute a dialog channel number.
    local key = ll.GetKey()
    -- Extract the last 7 characters of the key and convert it from hex.
    dialogChannel = -1 - tonumber(string.sub(tostring(key), -7, -1), 16)
end

-- Called when the object is touched.
function touch_start(num_detected)
    ToucherID = ll.DetectedKey(0)
    -- If there is already a listen handle, then remove it
    if listenHandle then
        ll.ListenRemove(listenHandle)
    end
    listenHandle = ll.Listen(dialogChannel, "", ToucherID, "")
    ll.Dialog(ToucherID, dialogInfo, buttons, dialogChannel)
    -- Set a 60-second timer for response.
    ll.SetTimerEvent(60.0)
end

-- Called when a dialog response is received.
function listen(channel, name, sender_id, message)
    if message == "-" then
        -- Redisplay the dialog if the "-" option is selected.
        ll.Dialog(ToucherID, dialogInfo, buttons, dialogChannel)
        return
    end
    -- Stop the timer, and stop the listening handler.
    ll.ListenRemove(listenHandle)
    ll.SetTimerEvent(0)
    -- Let the user know what they selected
    ll.Say(0, `You selected {message}`)
end

-- Called when the timer expires.
function timer()
    -- Stop the timer and clean up the listener.
    if listenHandle then
        ll.SetTimerEvent(0)
        ll.ListenRemove(listenHandle)
        ll.Whisper(0, "Sorry. You snooze; you lose.")
    end
end

-- Invoke state_entry on startup, since simulator doesn't invoke 
-- it like it does in LSL
state_entry()

user_input_coroutine.lua

This script demonstrates coroutines and how they can simplify the overarching logic of a script, enabling us to write the bulk of our multi-event code within a centralized function instead of fragmenting across separate event handlers.

-- Wait for user input mid-function before doing something useful with it.
main = function(toucher)
    local handle = ll.Listen(0, "", toucher, "")
    local event = touch_start   -- save function for later
    touch_start = nil           -- disable touch_start

    ll.RegionSayTo(toucher, 0, "Do you want pants or gloves?")
    local clothing = coroutine.yield() -- pause the routine's execution here
    ll.RegionSayTo(toucher, 0, "For men or women?")
    local gender = coroutine.yield()
    ll.RegionSayTo(toucher, 0, "Favorite color?")
    local color = coroutine.yield()
    ll.RegionSayTo(toucher, 0, "Here's "..color.." "..clothing.." for "..gender)

    ll.ListenRemove(handle)
    touch_start = event -- restore touch_start
end

function touch_start(total_num)
    local toucher = ll.DetectedKey(0)
    routine = coroutine.create(main)    -- new coroutine
    coroutine.resume(routine, toucher)  -- run coroutine (with one argument)
end

-- When the coroutine is suspended, incoming events can be handled
-- and we can resume() execution of the routine
-- and pass any number of arguments to be returned by yield()
function listen(channel, name, id, message)
    coroutine.resume(routine, message)
end

multi_user_input_coroutine.lua

Following from the above example, how can we handle multiple users? This is where coroutines shine.

Instead of disabling touches to prevent others from interacting with the object, we can create new copies of the coroutine each time an avatar touches the object. We can then resume whichever coroutine is needed, based on the avatar, while all of them track their own progress separately and automagically.

-- Key: avatar uuid; Value: coroutine thread
routines = {}

main = function(toucher)
    local handle = ll.Listen(0, "", toucher, "")

    ll.RegionSayTo(toucher, 0, "Do you want pants or gloves?")
    local clothing = coroutine.yield()
    ll.RegionSayTo(toucher, 0, "For men or women?")
    local gender = coroutine.yield()
    ll.RegionSayTo(toucher, 0, "Favorite color?")
    local color = coroutine.yield()
    ll.RegionSayTo(toucher, 0, "Here's "..color.." "..clothing.." for "..gender)

    ll.ListenRemove(handle)
    routines[toucher] = nil -- Remove from collection
end

function touch_start(total_num)
    local toucher = ll.DetectedKey(0)
    local routine = routines[toucher]
    if not routine then -- New user needs new routine
        routine = coroutine.create(main)
        routines[toucher] = routine -- Add to collection
        coroutine.resume(routine, toucher)
    end
end

function listen(channel, name, id, message)
    coroutine.resume(routines[id], message) -- Resume a specific coroutine
end

More Examples