Scripts (LSL through WorldBridge)
Scripts are how the world lives. WorldBridge set_script attaches LSL text to an object's root prim and starts it
under YEngine, the region's script engine, bypassing the permission gate that stops a non-present owner from adding
a script. Most builders in tools/ generate their LSL as Python strings and lay it with set_script.
Laying scripts
1. One behaviour, one script name
set_script REPLACES a script of the same name and ADDS a script with a different name alongside the old one. Two
scripts that both read-modify-write the same property race and clobber each other, silently; the last to run wins.
- Learned: clouds drifted backward until an old
clouddriftscript was neutralised; its replacement had a new name and both were moving the same prims. - Learned: two builders each put a script called "flicker" on the same flame; re-running the older builder would have replaced the newer fire driver.
- Rule: one owner per object and script name. When a builder takes an object over, remove it from the old builder's list with a comment saying who owns it now.
- Before laying a script that sets
PRIM_COLOR, grep for others on the same objects: two colour-setting scripts fight. Give the owner the colour; give an overlay (wetness) only the gloss.
2. started=true is not the compile verdict
set_script answers started=true even when YEngine rejects the script. The compile error goes to
the server's bin/OpenSim.log, logged BEFORE WorldBridge's own set_script line, and nothing runs. A script that
"does nothing" is a compile error until shown otherwise.
- A compile error is logged as "compile error" followed by
(line,col) Error: ...; a PARSE failure is worded differently ("reduction errors", then one(line,col) Error:per fault, any number of them). - Learned: the first guard read a fixed six-line window for "compile error" and passed a five-error parse failure as green.
- See:
tools/wb.py(_compile_errorreads the log back to the previousset_scriptline after everyset_scriptand raisesScriptCompileErrorwith the detail). Its log path is still the original checkout's (LOGintools/wb.py); on any other install it finds no log and skips the check silently, so editLOGto yourbin/OpenSim.log.
3. Templated scripts: fill every placeholder
Unfilled %PLACEHOLDER%s are the usual compile error ("invalid operand token" at the column of the %). In Python,
a + b + c.replace(...) fills only c; bracket the whole concatenation before the replace chain. Insert LSL that
uses % (modulo) with .replace, never with Python %-formatting.
- See:
tools/weather_common.py(STORM_LSL, inserted with.replace("%STORM%", STORM_LSL)).
4. Write if/else, not ?:
Standard LSL has no ternary. YEngine accepted one in a declaration but rejected llSetAlpha(flag ? 0.0 : 1.0, ...).
5. A touch handler added to an existing object needs its click flags resent
The viewer may not offer Touch until the object's flags are resent: call llSetClickAction(CLICK_ACTION_TOUCH) in
state_entry (it also makes a left-click a touch).
6. One-shot scripts leave the look and remove themselves
A script that sets faces, status or parameters and ends with llRemoveInventory(llGetScriptName()) leaves its
effect and no running script. tools/skin_cottage.py, tools/phantomize.py, tools/mask_cutoffs.py all use it.
Time and shared state
7. Use a shared clock to move separate scripts together
llGetGMTclock() (float seconds) is the same in every script. llGetTime() starts at each script's own reset, so
phases drift apart. llGetUnixTime() is shared but whole seconds. For sub-second shared phase, the build used the
fraction from llGetTimestamp.
- 26 blossoms, each with its own script, breathe as one on
llGetGMTclock(tools/build_spirit_kindle.py). - A formula every script evaluates on the same clock needs no controller:
weather_common.STORM_LSLcomputes the identical storm value in every prim, so clouds, mist and the stream cannot desync.
8. Durable memory lives in the object description
llGetObjectDesc / llSetObjectDesc survive script resets AND region restarts.
(integer)of a non-numeric description is 0 in LSL. An age memory that reads a planting time from the description needs a guard (if (planted < 1000000000), plant now), or garbage reads as "ancient" and the plant jumps to full size.- Never bake a base value from a live read that might be disturbed (a kindled blossom, a weathered stone, a wet face). Snap to the DESIGN values from the builder's source, or bake them into the script at lay time.
- Learned: a scale-mutating memory tested on live prims baked its own test disturbance as the new base, and it compounded.
9. One controller owns each accumulating state
Weather memory (snow depth, wetness, ice, icicles) belongs in ONE controller that integrates it on the shared clocks, persists it in its description, and broadcasts each value on its own channel. Tune the rates with a Python simulation of the real clocks first.
- Learned: the first wetness rates left the stone dark three quarters of the time. The snow simulation showed the region's winter storms run 44% of the time at a mean strength of 0.68, so a naive rate would saturate at full cover all winter.
- See:
tools/build_snowline.py(--sim),tools/build_wet.py.
10. The island's time of day in a script
The sky's time of day is the region's environment day, and OpenSim's LSL reads it: llGetRegionDayLength() and
llGetRegionDayOffset(); fraction = ((llGetUnixTime() + offset) % length) / length, 0.5 = midday. llGetTimeOfDay
is the legacy 4-hour cycle and does NOT follow the environment day. llGetSunDirection().z tells day from night.
- See:
tools/build_shipbell.py.
11. Do not switch many things at one threshold
Lights that switch at the same sun height all switch together, which reads as a switch. Give each its own threshold and the dusk becomes a keeper going round (dawn reverses it for free).
- See:
tools/build_living.py(LIT).
A world that notices you
12. Ease toward a visitor, then stop
llSensorRepeat("", NULL_KEY, AGENT, radius, PI, 1.5) finds the nearest visitor; ease a welcome toward
1 - distance/radius on the timer so things swell as you approach and settle after you leave, instead of
snapping. Stop the timer once settled so an unvisited thing costs nothing.
- See:
tools/build_living.py,tools/build_spirit_kindle.py.
13. One-shot reactions need hysteresis
Forget the visitor only after about 20 s out of range, or the edge of the radius rings a greeting over and over.
14. Carry every change in size and colour, not glow alone
Glow is a bloom effect the viewer dims or drops at modest graphics settings.
- Learned: a tree whose blossoms "breathed" almost wholly in glow read as "pretty static" orbs.
15. An NPC is an agent for sensors
Stand one somewhere (npc_create) to test anything presence-gated without a person.
Talking to scripts
16. Talk to scripts with chat_send (or wb.region_say)
WorldBridge chat_send delivers through the world-comm module, as an object's llSay is, so scripts' llListen
hear it (scripts, default true), and it speaks as the estate owner (from_uuid), so owner-checking listeners
accept it. wb.region_say(channel, msg) is chat_send with region=True (as llRegionSay).
- Learned: an earlier
chat_sendspoke as a null key and reached avatars only;region_saythen rezzed a transient estate-owned prim to say it. Neither is needed now. - See:
tools/wb.py(region_say),tools/sound_mode.py(set_mode).
17. Read a script's state back through its description or name
list returns each object's description as its shape field and its name as name. A script that writes its last
action or its computed values into llSetObjectDesc (or its object name) is readable with no console. (Object
llSay is not in WorldBridge's chat log, and console show object output goes to the console, not the log.)
18. Messages between objects
Objects talk on private channels (llRegionSay / llListen). Replacing an object does not break this: messages
reach the new one by channel as before. Two creatures sharing perches can announce the perch they take on a private
channel.
Testing slow and rare behaviour
19. Test a slow cycle with a fast copy on one prim
A 20-minute storm or a 4-day year cannot be watched. Deploy a FAST-period copy to ONE prim, watch the driven
property swing (colour and size via inspect / list), check it holds across a script re-set, then restore the
real one.
- Design every change so it has a channel you can read back: colour and size are readable through WorldBridge; glow and light are not.
20. Test a gated behaviour by moving its threshold
A weather-gated thing (rain, a rainbow, a strike) may be 40 minutes from its window. Re-lay its script with the
thresholds moved (a harness), watch it, then put the real thresholds back. Several builders take a command for
this (for example python tools/build_thunder.py strike <class> and python tools/build_shipbell.py strike 8).
21. Behavioural readback proves a timer is running
inspect a flickering flame's colour twice; if it changed, the timer is firing. Position readback of a fast
re-scripted prim is too noisy to measure a drift rate.
Limits you will meet
22. llSetRegionPos and PRIM_POSITION will not put a prim's centre below the ground
A mesh spawned with its centre under the terrain (a reed clump in the shallows) is lifted on its first scripted move (11 cm here). An emitter that should sit at water level over land cannot get there; search nearby for open water instead.
23. llWind is not a wind to build on
It is a weak random field (0.1-1.2 m/s), each 64 m patch its own direction, reshuffled about every 15 s. Give the
region ONE wind from a shared-clock formula and make everything read it (weather_common.WIND_LSL).
24. Particles exist only in the viewer
llParticleSystem makes fire, sparks, spray, mist and rain; verify it by looking in the viewer. An emitter prim is a
tiny sphere set to alpha 0. See weather-and-light.md.