Skip to content

Conversation

HaroldCindy
Copy link

@HaroldCindy HaroldCindy commented Aug 19, 2025

This contains the proposed API for event handling and timer management in SLua. Please add any suggestions inline on the code, or as a comment on this PR!

Particularly interested in how people think events like touch_start that pass a num_detected for ll.Detected*() function calls. Right now those events continue to receive a num_detected, but is there something better we should be doing there?

@LittleBitwise
Copy link

LittleBitwise commented Aug 19, 2025

The proposal looks good on an initial skim!

If I'm reading it right, there's support multiple timers in one script (awesome). What about ll.SensorRepeat, would it benefit from similar treatment?

@HaroldCindy
Copy link
Author

What about ll.SensorRepeat, would it benefit from similar treatment?

It might, but we don't have plans for it at present. The implementation for timer just ended up being pretty easy to tack onto the existing timer mechanism without messing around with a ton of server logic.

We'll have follow-on proposals for other things like sensors and async task management.

@WolfGangS
Copy link

WolfGangS commented Aug 19, 2025

Edit: I'll add these comments inline on the PR

snip

Just a check, the concept implementation makes use of os.clock().

If the actual implementation makes use of this, does that mean that os.clock() is consistent across multiple regions/hosts?

Afaik all the beta grid SLua sims are running on one machine, so it's not possible for me to test if it remains consistent.

The luau documentation states that os.clock doesn't have a "defined baseline". If SLua's implementation is based on something like system uptime, then it's not going to be suitable to the task for scripts that travel between regions and hosts.

For my own slua multi timer implementation I used ll.GetTime for now as that promises to be based on script "uptime", and preserves the type of behavior you see in a script, where a set timer of every 10 seconds if picked up after 5 seconds then rezzed out again would next trigger in 5 seconds, as ll.GetTime only "advances" while script is running.

function LLTimers:on(seconds: number, handler: EventHandler): LLTimersProto
assert(seconds > 0)
table.insert(self._timers, {
nextRun=os.clock() + seconds,
Copy link

@WolfGangS WolfGangS Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is os.clock consistent across hosts? Luau doesn't define a baseline for the time it returns only that it increments in seconds.

This would cause problems if it is for instance based on host uptime or something similar, and timers are being set on different hosts as the script travels/teleports from region to region.

This is of course only a problem if os.clock isn't cross host consistent, and the actual implementation uses it.

Copy link
Author

@HaroldCindy HaroldCindy Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual implementation won't rely on os.clock() being the same across hosts (though it should,) see https://github.com/secondlife/issues/pull/3/files#diff-03b63dc9a26d2a3d82b27fea35bfd7acb64b7681fdf1a5d877a096db5dadea3cR174-R176 . In the actual implementation, when the script state gets serialized we'll turn it into a timestamp relative to the current os.clock(), then the far end will treat the timestamps as relative to the current time at the time of deserialization. Timers only tick down while objects are in-world, not when they're in transit or inventory, so we have to do it that way.

Copy link

@WolfGangS WolfGangS Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh... it does help if I carefully read it all completely, sorry for wasting your time, in my head relative to os.clock was still a problem.

Copy link
Author

@HaroldCindy HaroldCindy Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, absolutely not! Thanks for thinking about these things!

Copy link
Author

@HaroldCindy HaroldCindy Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You bring up a good point, actually, with regards to ll.GetTime(), since that's the natural / correct way to write this.

The reason I try to avoid ll.GetTime() as much as possible is because of its poor output precision (32-bit float) and the fact that it can experience significant drift in as little as 4 hours due to the precision of the underlying timer. I only used os.clock() because there isn't a timer for seconds that a script has been "alive" like ll.GetTime() that doesn't suffer from those drift problems. I'll see if we can come up with a better API for that.

Copy link

@WolfGangS WolfGangS Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general a global table for "meta" functions/values would probably be useful in the future. Something that can contain metadata about the script itself and meta functions.

type llscript = {
    compileTime: number, -- Unix timestamp of script compile
    startTime: number, -- Unix Timestamp of script start (resets with ll.ResetScript)
    getUpTime: () -> number. -- Like ll.GetTime (without the issues and no reset function, except ll.ResetScript)

    bytecodeSize: number, --- The size of the bytecode
    ...
}

Just some ideas

Comment on lines +6 to +10
-- Event handlers which stack multiple events together in LSL via an integer `num_detected` parameter
-- TODO: Pass in a special wrapper object for these functions and loop through `num_detected` rather
-- than actually pass in `num_detected`? Seems like almost nobody uses these functions as
-- intended since it's annoying to have to write the loop, everyone just does `llDetectedWhatever(0)`
-- and silently drops the rest of the queued events of that type on the floor.
Copy link

@WolfGangS WolfGangS Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be elaborated on some?

In general I agree that the grouping of some events, is an unintuitive pattern, that is likely the result of resource management from the past.

But

Does this TODO: Pass in a special wrapper object for these functions mean that it would ALWAYS be done for the scripter, or is it more of a TODO: implement supporting this?

If this were to be implemented as the new standard it would need careful documenting else we will end up with people still doing ll.DetectedKey(0) but now having it run 10 times or something.

It would also need to pass in the current "event index" to allow scripters to use the correct data via ll.Detected...

Or a rework of how the ll.Detected... functions work, so as to not need an index, and pretend that that system doesn't exist. Where in reality something equivalent to the code below is executing, but the scripter only sees the bit after USER CODE themselves.

local LL = ll;
ll = setmetatable({}, {__index=ll});
(function()
    local detectedIndex = nil
    ll.SetDetectedIndex = function(index:number) detectedIndex = index end
    ll.DetectedKey = function(num:number)
        return LL.DetectedKey(num or detectedIndex)
    end
end)()

function touch_start(events : number)
    for i=0,events-1 do
        ll.SetDetectedIndex(i)
        touch_handler()
    end
end

-- USER CODE

touch_handler = function()
    ll.OwnerSay(`Touched By: {ll.DetectedKey()}`)
end

A "clearer" alternative might be to add new "pseudo" events, named something like each_touch_start or each_touch etc but that smells in other ways.

Copy link
Author

@HaroldCindy HaroldCindy Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it more of a TODO: implement supporting this?

Sorry about that, yeah, all TODOs are things that need to be fleshed out (or decided against) for this RFC.

Or a rework of how the ll.Detected... functions work, so as to not need an index, and pretend that that system doesn't exist. [...]

Yeah, we definitely want to get rid of ll.Detected*() if we can, since the usability story isn't great there.

I guess we're worried about the usability angle. It's not obvious that ll.DetectedKey() and friends are functions that you should think about calling in that context, or that something like ll.DetectedVel() is something that will work in some contexts but not others, and that their output is going to be nonsense if you try to use them in a coroutine once execution is no longer happening under the touch_start() handler.

Then you have events like sensor where you want want to handle all those detected items as a single batch, since you might want to select the closest object matching certain criteria or something.

My thinking was that there could be wrapper object that store an index internally, and that have methods like event:getKey() or event:getVel() and friends that get marked as invalid when execution leaves the relevant event handler.

These event objects would be inexpensive memory-wise (is_valid bool + event number index) so no resource worries there, just not sure what makes sense. Passing in a table of these event objects?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One idea could be returning results in tables for events like sensor and collision where there are routinely going to be multiple results.

The various Detected types could be stored in the list as a way to get rid of the Detected functions. Then llDetectedKey(0) becomes something like result[1].key.

This would not be as memory-efficient per event object though, so it has its drawbacks. Ease of use versus resource consumption.

Copy link

@ThunderRahja ThunderRahja Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware, some scripts call llDetected*(0) without a proper loop when they really should be looped (discarding incidents that built up during a sleep or forced delay), while some scripts deliberately accept only the first input and then discard the rest. If touch*, collision*, sensor and other events that provide values for the llDetected* functions are restructured to call a user-defined function (handler) once for every incident, then SLua must not pay additional forced delay time for each time the UDF is called within what would be a batch in LSL. The script should drop further UDF calls if removed from the event before a batch is finished.

Scripters can always implement their own wrappers in SLua. Time will tell whether this is actually necessary. Some will do it out of habit, but I think most will take the path of least resistance. I have never considered it annoying to wrap llDetected* functions in a loop; batching reduces the number of events in the queue, and fewer events in the queue means a more responsive script.

Copy link

@WolfGangS WolfGangS Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeh the more I think about this, the more it feels like this should be a new event, and the old events should be marked as "deprecated".

I also like Kristy's idea of "clicked"

Maybe we can have something like this instead

local handler = function(event: TouchedEvent)
    ll.OwnerSay(lljson.encode(event)) -- {"type":"start","key":"<uuid>","link":1,"uv":"<0.5,0.5,0.5>",...}
end
LLEvents:on("touched", handler)

type should probably be a number/enum, but I used a string for clarity

This could later be expanded to allow for filtering...

LLEvents:on("touched", handler, {type="end", key=ll.GetOwner()})

end

function LLTimers:on(seconds: number, handler: EventHandler): LLTimersProto
assert(seconds > 0)
Copy link

@KrsityKu KrsityKu Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this (and also the once version) should not assert and instead trigger the off behaviour to match what llSetTimerEvent(0) does.
This way we can use

llTimers:on(CalcMyNextTimer(), myHandler)

instead of:

local next = CalcMyNextTimer()
if next > 0 then
	llTimers:on(next, myHandler)
else
	llTimers:off(myHandler)
end

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably won't, it would be surprising for new scripters to have a method called :on() that sometimes unsubscribes from an event. This would also be in keeping with the EventEmitter semantics that people are probably used to.

@KrsityKu
Copy link

Particularly interested in how people think events like touch_start that pass a num_detected for ll.Detected*() function calls. Right now those events continue to receive a num_detected, but is there something better we should be doing there?

is adding a clicked event off the table?

In my scripts I use touch_end most of the time, as all I do is just open a menu.
I do have one script that does proper touch_start touch touch_end sequence, but that's way less common. So I think it might be best not to mess with click and hold/drag functionality too much.

Having a simple clicked event could simplify things, instead of llDetected*, it could just have UUID, LinkID, FaceID, etc as parameters. And if multiple people do somehow manage to click it during the same simulator frame, it can easily trigger 2 separate events.

Though, that only covers the touch part of things, collisions are different beast...

Side note: maybe SLua having few extra handy events, compared to LSL, would encourage more creators to update their scripts or adopt new stuff faster to take advantage of the new features.

@Suzanna-Linn
Copy link

Some ideas...

Could we have states?

  • storing the handlers by state and event
  • an optional parameter in the methods with the state name (or "default")
  • LLEvents:state(state_name) to change the state

"num_detected" events

  • I like the idea of some kind of wrapper for touches and collisions to receive single events. For new students is very confusing, they have to learn touch_start in the first class and the num_detected becomes a problem.
  • But not for the sensor. where is more suitable to receive all the detections together.

LLEvents:off / LLTimers:off

  • I don't like that adding the same handler function again duplicates it, but it's how the EventEmitter is done.
  • Anyway, why, in case of duplication, LLEvents:off removes the first one and LLTimers:off removes the last one?

LLEvents:once and LLEvents:off

  • It sounds well to be able to :off a :once event, but it would be inefficient and not really necessary.
  • If we want one event we can use :once, if we are not sure we can use :on, and :off when needed.

LLEvents:listeners

  • What is its use?... calling events in the script? checking if a function is already there?

And two questions to know how to teach the changes to the students:

  • will the current Slua events format still work after adding LLEvents (to be removed in the beta version) or will it be removed immediately?
  • will the event timer (in LLEvents) and ll.SetTimerEvent() work in the future?

Depending on it i will teach the changes to the students before LLEvents arrives (so the students know what to do when their scripts stop working) or after LLEvents arrives (if the scripts will go on working for some time, better for explaining and testing).

Thanks Harold, great project!

@HaroldCindy
Copy link
Author

Could we have states?

We're not planning to provide this ourselves during the first effort. We're trying to only build out the things that people can't efficiently write themselves in SLua first, then adding wrappers around common usage patterns as we see them crop up in real code. Mostly because there's only a handful of folks working on SLua, and we still have a bunch of "must-haves" to implement :P

States could be pretty easily done in user code by having states represented as a table of string -> handler. States also become tricky now that you can dynamically attach / detach event handlers. Does switching states unregister all of those, or only handler that are explicitly defined on the state you're switching from? Does it unregister all of the dynamic timers?

"num_detected" events

Yeah, that seems sensible. It seems like the existing event batching is something people want, but I think we can do it with slightly better usability. We're going to look at writing a wrapper around the existing num_detected stuff that passes in a table of lightweight event objects that are easier to use rather than making people call ll.Detected*() on an integer index. I'll update this PR with example code for people to review once that's ready.

Anyway, why, in case of duplication, LLEvents:off removes the first one and LLTimers:off removes the last one?

This was caused by me not paying attention, I'll fix it :)

LLEvents:listeners

It's mainly for debugging. With LSL you always know what handlers are active since all handlers are grouped by state and you can't dynamically add or remove them, but that's not the case in SLua.

will the current Slua events format still work after adding LLEvents (to be removed in the beta version) or will it be removed immediately?

They'll be removed immediately, it'll be a breaking API change once it's implemented

will the event timer (in LLEvents) and ll.SetTimerEvent() work in the future?

Likely not. The LLTimers scheduler will use the same mechanism as ll.SetTimerEvent() under the hood, and if you have access to LLTimers there's not much reason to use ll.SetTimerEvent().

@Suzanna-Linn
Copy link

To script states in LSL-style, could we have functions like:

  • ClearEventQueue() : to clear the pending events in the queue
  • ListenRemoveAll() : to remove all the active listeners

Is this related to this project? Or do I write a canny request for new LL functions?

@HaroldCindy
Copy link
Author

ClearEventQueue() : to clear the pending events in the queue

I believe you'll get much more targeted event dropping under the new scheme. Removing all event handlers for a particular event clears the queue of all events of that type once this is actually implemented in C++. Is there another usecase that ClearEventQueue() would address?

ListenRemoveAll() : to remove all the active listeners

We're going to follow up with an LLListeners API or something similar that would enable the same functionality and do the ll.ListenRemove() for you, would that be sufficient?

@WolfGangS
Copy link

WolfGangS commented Aug 25, 2025

@HaroldCindy I'm not sure, but I think @Suzanna-Linn's ListenRemoveAll may have been more targeted at LLEvents:listeners

A means to drop/cleanup all registered event handlers at once, could be useful.

@KrsityKu
Copy link

In one of my scripts I use state swapping to clear all the listener handles because very very rarely, my script ends up running out of listeners, and I can't figure out how to reproduce that issue, as far as I can see all my code paths create and remove listeners just fine. I have since rewritten the script in SLua, so hopefully I won't run into that issue.

But something like ListenRemoveAll() would be neat to have, kind of as insurance that no handles are being leaked etc.

@HaroldCindy
Copy link
Author

HaroldCindy commented Aug 26, 2025

Makes sense to me! Once the event API is finalized / merged we can look at more utilities for clearing the sim-side script event queue if they're still necessary, I don't think any of this work would block that.

@Suzanna-Linn
Copy link

ClearEventQueue() : to clear the pending events in the queue

I believe you'll get much more targeted event dropping under the new scheme. Removing all event handlers for a particular event clears the queue of all events of that type once this is actually implemented in C++. Is there another usecase that ClearEventQueue() would address?

Good idea, clearing the pending events of a specific type when all its handlers have been removed is perfect.

ListenRemoveAll() : to remove all the active listeners

We're going to follow up with an LLListeners API or something similar that would enable the same functionality and do the ll.ListenRemove() for you, would that be sufficient?

Yes, that would be sufficient if it includes a function to remove all listeners without needing to know their individual handles. This would be useful for simulating a state change, as it would avoid the need to wrap ll.Listen() to store the handles.

Although a ll.ListenRemoveAll() would have a more general use, also in LSL, so lazy scripters (like me) wouldn't leave listeners active so often.

@Suzanna-Linn
Copy link

@HaroldCindy I'm not sure, but I think @Suzanna-Linn's ListenRemoveAll may have been more targeted at LLEvents:listeners

A means to drop/cleanup all registered event handlers at once, could be useful.

Actually, I was referring to removing all the listeners activated with ll.Listen(), which would allow for simulating a state change in an LSL-style.

Although having a parameterless LLEvents:off() to remove all events would be good too, and the same goes for LLTimers:off().

@HaroldCindy
Copy link
Author

Although having a parameterless LLEvents:off() to remove all events would be good too

We don't want to do too much special logic with missing arguments (since they're just filled with nil and someone could have done that by accident, which'd be hard to debug) but you can basically do that with

for i, event_name in LLEvents:eventNames() do
  LLEvents:off(event_name)
end

-- Find the handler first, then remove it
local found_index = nil
for i = 1, #handlers do
if handlers[i] == handler then

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where a scripter does something like this.

LLEvents:on("touch", function()
    ll.OwnerSay("Touched")
end)

It becomes impossible to off that anonymous function, without offing all handlers. (Same of LLTimers too)

Options are either

  • Document and explain to scripters in advance that they will face this "problem"
  • Return some other type of "handle" value from on that can be used to unsubscribe (a la JS const timer = setInterval(() => clearInterval(timer));)

Both are obviously valid choices, just think it should be addressed as a conscious decision rather than just an assumed "default"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the documentation approach more. It feels more natural to name the function before on'ing it, if it has to be off'ed, than having two ways to off (by function name or by returned handle).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think the current method should keep functioning as is too, just wanted to address the anonymous function case specifically, either to document it as Intended, or see if it want's handling.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, setInterval() returning an opaque handle from JS is a pretty old API before people were thinking too much about API symmetry. We'll go with the documentation approach.

@Jamesp1989SL
Copy link

This contains the proposed API for event handling and timer management in SLua. Please add any suggestions inline on the code, or as a comment on this PR!

Particularly interested in how people think events like touch_start that pass a num_detected for ll.Detected*() function calls. Right now those events continue to receive a num_detected, but is there something better we should be doing there?

@HaroldCindy does this system allow for custom event handlers or is this out of the scope of the proposal

@HaroldCindy
Copy link
Author

Custom event handlers are out of scope for this since the underlying event system SL is using doesn't support them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants