What is new in the release 2026-07-24
There is a new SLua release in the SLua regions on the Main Grid!
I’ve gathered all the info I could find about the changes coming in this release.
Scripts don’t need changes or to be recompiled to work with this release.
(this page updated on Tuesday, July 28th)
LLTimers
The behavior of LLTimers has changed: it no longer attempts to catch up on missed ticks when the delay is under 2 seconds.
To prevent timing drift, LLTimers normally tries to preserve the scheduled cadence (calculated as previous schedule + interval). However, a timer event can be delayed if the script is executing other events, or if the script is paused and resumed (e.g., during teleports, region crossings, attachments/detachments, rez/derez events, or region restarts).
When a delay occurs:
- If the next scheduled run time is in the future (relative to the current time), the original cadence is preserved.
- If the next scheduled run time is already in the past, the timer does not fire repeatedly in quick succession to catch up. Instead, it resets its cadence relative to the current time (now + interval).
Next timer in the future, cadence preserved:
-- pause (a teleport)
LLTimers:every(5, function(expected, interval)
print(string.format("%18.15f%5.1f%20.15f", expected, interval, ll.GetTime()))
end)
--[[
expected time int current time
5.667029000003822 5.0 5.689430999977048
10.667029000003822 5.0 10.689374000008684
15.667029000003822 5.0 15.689446999982465
( a teleport here )
20.667029000003822 5.0 22.911421999975573
( next timer in the future, cadence preserved)
25.667029000003822 5.0 25.689406000019517
30.667029000003822 5.0 30.689406000019517
]]
Next timer in the past, cadence reset, no catch-up:
-- pause (a teleport)
LLTimers:every(1, function(expected, interval)
print(string.format("%18.15f%5.1f%20.15f", expected, interval, ll.GetTime()))
end)
--[[
expected time int current time
2.911445999983698 1.0 2.933622999989893
3.911445999983698 1.0 3.933587000006810
4.911445999983698 1.0 4.933663000003435
( a teleport here )
5.911445999983698 1.0 8.111338999995496
( next timer in the past, cadence reset)
9.111338999995496 1.0 9.111368999991100
10.111338999995496 1.0 10.133544999989681
]]
Metamethod __call
The __call metamethod is never used as an iterator.
While Luau maintains this behavior to preserve backwards compatibility with legacy custom iterators, it is not necessary in SLua.
Previously, to use generalized iteration on a table that implemented a __call metamethod, we had to explicitly define an __iter metamethod (such as __iter = pairs)
llprim library
The new llprim library is designed to bridge the gap between LSL’s flat, list-based parameter structures and Lua’s advamced data structures.
The llprim library introduces structured alternatives, such as a fluent builder interface and dictionary tables, which make script configurations cleaner, more readable, and easier to manipulate dynamically.
Currently it has:
- llprim.ParamsSetter : A builder class that wraps ll.SetLinkPrimitiveParamsFast() (as well as SetPrimitiveParams() and SetLinkPrimitiveParams()), enabling method chaining.
- llprim.setMedia() : A dictionary-based wrapper for ll.SetLinkMedia() and ll.SetPrimMediaParams()
- llprim.setParticleSystem() : A dictionary-based wrapper for ll.LinkParticleSystem() and ll.ParticleSystem()
The llprim library doesn’t add new functionality but allows us to work in a more Lua way.
llprim.ParamsSetter
Wrapper of ll.SetLinkPrimitiveParamsFast() (and also SetPrimitiveParams() and SetLinkPrimitiveParams())
llprim.ParamsSetter is a builder class that simplifies the construction of primitive parameter lists. Instead of manually assembling a flat table of PRIM_ constants, we instantiate a helper object and chain descriptive methods together.
Each method call appends its corresponding rule to an internal table and returns the helper object itself (self), enabling a fluent interface. The accumulated changes are deferred and then sent to the simulator atomically in a single native call when we invoke the :apply() method.
Basic Usage and Immediate Application
For simple modifications, we can instantiate the builder, chain the desired property changes, and apply them immediately. Each method has the same parameters as the constants:
-- Simple use, apply params immediately
llprim.ParamsSetter.new()
:targetLink(LINK_ROOT)
:color(ALL_SIDES, vector(0,0,1), 1)
:text("first", vector.one, 1)
:apply()
Deferred and Batch Construction
Because the builder accumulates rules in an underlying table before execution, we do not have to apply changes immediately. We can define a builder, populate it dynamically inside loops or conditional blocks, and trigger the update atomically when ready. This is highly efficient, as it reduces the overhead of making multiple, separate LSL-style parameter calls:
-- But we can also collect rules to build upon and apply later
-- Here we arrange the child links in a line above the root
local rules = llprim.ParamsSetter.new()
for i=2,ll.GetNumberOfPrims() do
-- The method calls really just append an SPP rule then return **self**
rules
:targetLink(i)
:pos(ll.GetPos() + vector(0,0,1 * i))
end
-- And we apply the changes atomically as one `ll.SetLinkPrimitiveParamsFast()` under the hood.
rules:apply()
Manipulating the Rule Set
Under the hood, a ParamsSetter instance is a standard Lua table array. This allows us to use library functions like table.extend() to merge rules from other sources, or to append raw LSL constants manually:
-- We can use `table.extend()` to mash rules onto it from other lists or builders
table.extend(rules, {PRIM_PHYSICS, false})
Cloning Configurations
Since the builder is a table, we can duplicate an existing set of rules using table.clone(). This allows us to define a base template configuration and modify it for different target links or sides without rebuilding the entire parameter set from scratch:
-- Cloning is fine too
local rules2 = table.clone(rules)
rules2
:targetLink(LINK_THIS)
:text("We can tack rules onto the clone", vector.one, 1.0)
:apply()
Coercing Tables into Builders If we have an existing raw parameter list, we do not need to rewrite it to use the fluent interface. By setting its metatable to llprim.ParamsSetter, we upgrade the table into a builder, gaining access to all of its chaining methods and the :apply() function.
-- The source table can come from anywhere, we can just give it the metatable to turn it into a builder.
local someOtherTable: {any} = {PRIM_POSITION, ll.GetPos() + vector.one}
setmetatable(someOtherTable, llprim.ParamsSetter)
someOtherTable
:text("And now it's a ParamsSetter", vector.one, 1)
:apply()
Method Name Mapping
The methods available on ParamsSetter are derived from the standard PRIM_ constants, with the prefix removed and in camelCase. Several have been shortened or updated to be more intuitive. Unused or deprecated constants have been excluded:
| Constant Name | Method Name | Change |
|---|---|---|
| PRIM_NAME | name | |
| PRIM_DESC | description | Changed |
| PRIM_TYPE | type | |
| PRIM_SLICE | slice | |
| PRIM_PHYSICS_SHAPE_TYPE | physicsShapeType | |
| PRIM_MATERIAL | physicsMaterial | Changed |
| PRIM_PHYSICS | physical | Changed |
| PRIM_TEMP_ON_REZ | temporary | Changed |
| PRIM_PHANTOM | phantom | |
| PRIM_POSITION | pos | Changed |
| PRIM_POS_LOCAL | posLocal | |
| PRIM_ROTATION | rot | Changed |
| PRIM_ROT_LOCAL | rotLocal | |
| PRIM_SIZE | size | |
| PRIM_TEXTURE | texture | |
| PRIM_RENDER_MATERIAL | renderMaterial | |
| PRIM_TEXT | text | |
| PRIM_COLOR | color | |
| PRIM_BUMP_SHINY | shinyBump | Changed |
| PRIM_POINT_LIGHT | pointLight | |
| PRIM_REFLECTION_PROBE | reflectionProbe | |
| PRIM_FULLBRIGHT | fullbright | |
| PRIM_FLEXIBLE | flexible | |
| PRIM_TEXGEN | texgen | |
| PRIM_GLOW | glow | |
| PRIM_OMEGA | omega | |
| PRIM_NORMAL | normal | |
| PRIM_SPECULAR | specular | |
| PRIM_ALPHA_MODE | alphaMode | |
| PRIM_LINK_TARGET | targetLink | Changed |
| PRIM_CAST_SHADOWS | - | Deprecated |
| PRIM_LEGACY | - | Deprecated |
| PRIM_ALLOW_UNSIT | allowUnsit | |
| PRIM_SCRIPTED_SIT_ONLY | scriptedSitOnly | |
| PRIM_SIT_TARGET | sitTarget | |
| PRIM_PROJECTOR | projector | |
| PRIM_CLICK_ACTION | clickAction | |
| PRIM_GLTF_BASE_COLOR | gltfBaseColor | |
| PRIM_GLTF_NORMAL | gltfNormal | |
| PRIM_GLTF_METALLIC_ROUGHNESS | gltfMetallicRoughness | |
| PRIM_GLTF_EMISSIVE | gltfEmissive | |
| PRIM_SIT_FLAGS | sitFlags | |
| PRIM_DAMAGE | damage | |
| PRIM_HEALTH | health | |
| PRIM_COLLISION_SOUND | collisionSound |
llprim.setMedia()
Wrapper of ll.SetLinkMedia() and ll.SetPrimMediaParams()
The llprim.setMedia() function wraps the configuration of face media (Shared Media). Instead of passing a flat array of alternating PRIM_MEDIA_ constants and values, this wrapper allows us to define the configuration using a structured Lua dictionary (a table with key-value pairs).
The function accepts three parameters:
- The target face number.
- A configuration dictionary table containing the media settings.
- An optional link target, which defaults to LINK_THIS if omitted.
This wrapper handles the necessary translation from dictionary keys to LSL media constants under the hood.
Be aware that any unrecognized keys in the dictionary are silently ignored during execution. Because the runtime does not throw a script error for invalid keys, typographical errors can be difficult to detect. Always double-check your spelling against the reference table:
local link = nil
llprim.setMedia(0, {
current_url = "https://example.com/",
home_url = "https://example.com/",
auto_play = true,
auto_loop = false,
controls = PRIM_MEDIA_CONTROLS_STANDARD,
width_pixels = 1024,
height_pixels = 768,
whitelist_enable = true,
whitelist = { "example.com", "*.example.com" },
perms_interact = PRIM_MEDIA_PERM_ANYONE,
perms_control = PRIM_MEDIA_PERM_OWNER,
}, link)
-- Note, the first parameter is the face. The last parameter is the link number. If link number is omitted
-- it defaults to LINK_THIS
Key Name Mapping
The configuration keys map directly to the native PRIM_MEDIA_ constants, with the prefix removed and in lowercase. Several have been shortened or updated to be more intuitive:
| Constant Name | Key Name | Change |
|---|---|---|
| PRIM_MEDIA_ALT_IMAGE_ENABLE | alt_image_enable | |
| PRIM_MEDIA_CONTROLS | controls | |
| PRIM_MEDIA_CURRENT_URL | current_url | |
| PRIM_MEDIA_HOME_URL | home_url | |
| PRIM_MEDIA_AUTO_LOOP | auto_loop | |
| PRIM_MEDIA_AUTO_PLAY | auto_play | |
| PRIM_MEDIA_AUTO_SCALE | auto_scale | |
| PRIM_MEDIA_AUTO_ZOOM | auto_zoom | |
| PRIM_MEDIA_FIRST_CLICK_INTERACT | first_click_interact | |
| PRIM_MEDIA_WIDTH_PIXELS | width | Changed |
| PRIM_MEDIA_HEIGHT_PIXELS | height | Changed |
| PRIM_MEDIA_WHITELIST_ENABLE | whitelist_enable | |
| PRIM_MEDIA_WHITELIST | whitelist | |
| PRIM_MEDIA_PERMS_INTERACT | perms_interact | |
| PRIM_MEDIA_PERMS_CONTROL | perms_control |
llprim.setParticleSystem()
Wrapper of ll.LinkParticleSystem() and ll.ParticleSystem()
The llprim.setParticleSystem function simplifies the creation and modification of particle effects. Instead of building the complex, flat list structure required by native particle functions, we pass a single dictionary of properties.
The function takes two parameters:
- A configuration dictionary table containing the particle rules.
- An optional link target, which defaults to LINK_THIS if omitted.
This wrapper handles the necessary translation from dictionary keys to LSL particle system constants under the hood.
Be aware that any unrecognized keys in the dictionary are silently ignored during execution. Because the runtime does not throw a script error for invalid keys, typographical errors can be difficult to detect. Always double-check your spelling against the reference table:
llprim.setParticleSystem({
pattern = PSYS_SRC_PATTERN_ANGLE_CONE,
texture = "da8e96f5-4ada-4f37-bd2c-cf3e68c49a42",
color_begin = vector(1, 0.6, 0.1),
color_end = vector(1, 0.1, 0),
alpha_begin = 1.0,
alpha_end = 0.0,
scale_begin = vector(0.1, 0.1, 0),
scale_end = vector(0.4, 0.4, 0),
glow_begin = 0.5,
glow_end = 0.0,
accel = vector(0, 0, 0.5),
burst_speed_min = 0.5,
burst_speed_max = 1.5,
burst_rate = 0.05,
burst_count = 5,
burst_radius = 0.1,
src_max_age = 0, -- 0 = continuous
part_max_age = 3.0,
color_interp = true,
scale_interp = true,
emissive = true,
wind = false,
}, link)
-- note link is an optional parameter. If missing it defaults to LINK_THIS
Key Name Mapping
The configuration keys map directly to the native PSYS_ constants, with the prefix removed and in lowercase. Several have been shortened or updated to be more intuitive:
| Constant Name | Key Name | Change |
|---|---|---|
| PSYS_PART_FLAGS | part_flags | |
| PSYS_SRC_PATTERN | src_pattern | |
| PSYS_SRC_BURST_RADIUS | src_burst_radius | |
| PSYS_SRC_ANGLE_BEGIN | src_angle_begin | |
| PSYS_SRC_ANGLE_END | src_angle_end | |
| PSYS_SRC_INNERANGLE | angle_inner | Changed |
| PSYS_SRC_OUTERANGLE | angle_outer | Changed |
| PSYS_SRC_TARGET_KEY | src_target_key | |
| PSYS_PART_START_COLOR | color_begin | Changed |
| PSYS_PART_END_COLOR | color_end | Changed |
| PSYS_PART_START_ALPHA | alpha_begin | Changed |
| PSYS_PART_END_ALPHA | alpha_end | Changed |
| PSYS_PART_START_SCALE | scale_begin | Changed |
| PSYS_PART_END_SCALE | scale_end | Changed |
| PSYS_SRC_TEXTURE | src_texture | |
| PSYS_PART_START_GLOW | glow_begin | Changed |
| PSYS_PART_END_GLOW | glow_end | Changed |
| PSYS_PART_BLEND_FUNC_SOURCE | part_blend_func_source | |
| PSYS_PART_BLEND_FUNC_DEST | part_blend_func_dest | |
| PSYS_SRC_MAX_AGE | src_max_age | |
| PSYS_PART_MAX_AGE | part_max_age | |
| PSYS_SRC_BURST_RATE | src_burst_rate | |
| PSYS_SRC_BURST_PART_COUNT | src_burst_part_count | |
| PSYS_SRC_ACCEL | src_accel | |
| PSYS_SRC_OMEGA | src_omega | |
| PSYS_SRC_BURST_SPEED_MIN | src_burst_speed_min | |
| PSYS_SRC_BURST_SPEED_MAX | src_burst_speed_max |
Parameters as dictionaries
To complement the helper functions in the llprim library, several native LL functions have been enhanced to natively accept dictionaries as parameters. This allows us to write clean, structured key-value tables directly inside standard LSL function calls, eliminating the need to construct or flatten arrays.
ll.SetPrimMediaParams() and ll.SetLinkMedia()
The media functions ll.SetPrimMediaParams() and ll.SetLinkMedia() accept a dictionary as their configuration parameter. This dictionary uses the exact same keys as the llprim.setMedia() wrapper:
ll.SetLinkMedia(0, 0, {
current_url = "https://example.com/",
home_url = "https://example.com/",
auto_play = true,
auto_loop = false,
controls = PRIM_MEDIA_CONTROLS_STANDARD,
width = 1024,
height = 768,
whitelist_enable = true,
whitelist = { "example.com", "*.example.com" },
perms_interact = PRIM_MEDIA_PERM_ANYONE,
perms_control = PRIM_MEDIA_PERM_OWNER,
})
The keys used in the dictionary table are the same as llprim.setMedia().
ll.ParticleSystem() and ll.LinkParticleSystem()
ll.ParticleSystem() and ll.LinkParticleSystem() accept a structured dictionary instead of a flat list. The keys and rules are identical to those defined for llprim.setParticleSystem():
ll.LinkParticleSystem(0, {
pattern = PSYS_SRC_PATTERN_ANGLE_CONE,
texture = "da8e96f5-4ada-4f37-bd2c-cf3e68c49a42",
color_begin = vector(1, 0.6, 0.1),
color_end = vector(1, 0.1, 0),
alpha_begin = 1.0,
alpha_end = 0.0,
scale_begin = vector(0.1, 0.1, 0),
scale_end = vector(0.4, 0.4, 0),
glow_begin = 0.5,
glow_end = 0.0,
accel = vector(0, 0, 0.5),
burst_speed_min = 0.5,
burst_speed_max = 1.5,
burst_rate = 0.05,
burst_count = 5,
burst_radius = 0.1,
src_max_age = 0, -- 0 = continuous
part_max_age = 3.0,
color_interp = true,
scale_interp = true,
emissive = true,
wind = false,
})
The keys used in the dictionary table are the same as llprim.setParticleSystem().
ll.HTTPRequest()
Constructing metadata, headers, and options for HTTP requests in LSL can be cumbersome due to the alternating parameter list format. In SLua, ll.HTTPRequest() accepts a structured dictionary for its options parameter.
This dictionary allows us to configure HTTP methods, custom headers, content types, and validation settings in an organized, readable block.
CSV strings and key-value map strings can be written as Lua arrays and dictionaries.
Be aware that any unrecognized keys in the dictionary are silently ignored during execution. Because the runtime does not throw a script error for invalid keys, typographical errors can be difficult to detect. Always double-check your spelling against the reference table:
local request_id =ll.HTTPRequest(
"https://api.example.com/data",
{
method = "POST",
mimetype = "application/json",
verify_cert = true,
extended_error = true,
custom_header = {
["X-Api-Key"] = "my-secret-key",
["X-Request-Source"] = "second-life",
},
accept = { "application/json", "text/plain" },
},
'{"hello": "world"}'
)
print(request_id)
There is no new library function to wrap ll.HTTPRequest().
Key Name Mapping
The configuration keys map directly to the native HTTP_ constants, with the prefix removed and in lowercase. Several have been shortened or updated to be more intuitive:
| Constant Name | Key Name | Change |
|---|---|---|
| HTTP_METHOD | method | |
| HTTP_MIMETYPE | mimetype | |
| HTTP_BODY_MAXLENGTH | max_body_length | Changed |
| HTTP_VERIFY_CERT | verify_cert | |
| HTTP_VERBOSE_THROTTLE | verbose_throttle | |
| HTTP_CUSTOM_HEADER | custom_header | |
| HTTP_PRAGMA_NO_CACHE | pragma_no_cache | |
| HTTP_USER_AGENT | user_agent | |
| HTTP_ACCEPT | accept | |
| HTTP_EXTENDED_ERROR | extended_error |