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.

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()})

Copy link
Author

Choose a reason for hiding this comment

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

Updated with an example of how wrappers for the num_detected type event handlers could work

@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

@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

@Martin-Pitt
Copy link

Martin-Pitt commented Sep 8, 2025

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?

Instead of num_detected it would be easier to script in a linear pattern across the touch_start->touch->touch_end pipeline. Especially for processing multiple and different touches. So instead of num_detected it would be a index_detected — this means you don't have to deal with a for loop.

Also the idea for a "click" event is pretty good as this could provide a standard confirmed click. (This should work like click on the web to meet existing and expected UX, where if you press and hold down on a button but then move and let go outside the button does not result in a click)

@HaroldCindy
Copy link
Author

HaroldCindy commented Sep 10, 2025

I think that adding a click event makes a lot of sense for the reasons people have mentioned, but it's probably out of scope for the initial SLua event handling proposal. Doing that would require some meddling with the core event handling logic in the simulators (that's scheduled for later,) whereas this is work is just building a bridge between SLua's view of events and the simulator's view of events.

@HaroldCindy
Copy link
Author

Pushed an update that changes how touch events and other num_detected event handlers work, let me know if y'all have any thoughts!

@Suzanna-Linn
Copy link

The new num_detected events are elegant and Lua-ish, but I wonder whether the improvement is really worth it, given that LSL and SLua will coexist for years. Having two different ways to do the same thing might be confusing, especially for less advanced scripters.

Anyway, I think that we can still use the LSL-style by adding a line at the beginning of the event handler:
num = #evts -- if they are 1-indexed
or
num = #evts + 1 -- if they are 0-indexed

@HaroldCindy
Copy link
Author

HaroldCindy commented Sep 10, 2025

The problem with keeping things as they are (just calling ll.Detected*() with an index and hoping you get something sensible back) is a problem in SLua specifically because coroutines are now a thing. With coroutines, logic kicked off within a single event handler might continue execution across multiple event handlers, and the results returned by ll.Detected*() are tied to the currently executing event handler.

With coroutines, it's much more likely that someone will store an event index and try to use it with ll.Detected*() when it no longer makes sense to. The wrapper is less about making things Lua-ish or elegant and more about making sure people get a nice big warning when they try to do something that would just return nonsense data now that we have first-level functions and more potential for transparently async code.

I can make an example of what I mean by that if it'd be helpful to illustrate the issue

@WolfGangS
Copy link

WolfGangS commented Sep 10, 2025

@HaroldCindy I assume this is the issue with ll.Detected... that you mean?

local last = nil

function touch_start(touches: integer | number)
    if last then 
        last()
        last = nil
    end
    ll.OwnerSay(`Clicked on face {ll.DetectedTouchFace(0)}`)
    last = coroutine.wrap(function() 
        ll.OwnerSay(`Last clicked on face {ll.DetectedTouchFace(0)}`)
    end)
    ll.OwnerSay("-------------------------------------------------------")
end

The code in the coroutine "feels" like it should print the detected face of when it was created, but will always print the value for the "latest" touch.

Another example would possibly be

local coro = nil

function touch_start(touches: integer | number)
    coro = coroutine.wrap(function() 
        ll.OwnerSay(`Clicked By: {ll.DetectedName(0)}`)
    end)
    ll.SetTimerEvent(0.01);
end

function timer()
    coro()
    ll.SetTimerEvent(0.0)
end

Where it prints, Clicked By: 00000000-0000-0000-0000-000000000000 which "feels" wrong, but isn't because ll.Detected is running the the scope of the current event, timer, which doesn't have detected

@HaroldCindy
Copy link
Author

@WolfGangS Yep, exactly

end

-- Do the actual event handling
for i, handler in handlers do
-- We explicitly want errors to bubble up to the global error handler, no pcall().
handler(event_args)
handler(table.unpack(event_args))

Choose a reason for hiding this comment

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

Could something like this be useful to avoid calling the next handlers when one returns true, indicating it has processed the event?

if handler(table.unpack(event_args)) then break end

For example, when multiple dataserver handlers are waiting for their Id, or multiple listen handlers are waiting for their channel.

Copy link

@WolfGangS WolfGangS Sep 11, 2025

Choose a reason for hiding this comment

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

I like it from the side of optimization, if we aren't going to get more advanced event filtering yet.

But then dislike that code somewhere completely different can prevent a handler from running, and it can change depending on the order handlers register. Though that would be a bad implementation on the scripters part really.

In general I would like @Suzanna-Linn's suggested behaviour.

Copy link
Author

Choose a reason for hiding this comment

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

But then dislike that code somewhere completely different can prevent a handler from running, and it can change depending on the order handlers register.

That's mainly what I'm concerned about, I'm not aware of functionality like this in any event handling APIs in other languages I've looked at. Just because the function you're calling is done with the message, doesn't mean there are no handlers following yours that might be interested in handling that message, and this is especially true in cases where people start using libraries for writing SLua that they didn't write.

I'll have to think on this a little bit, but my gut feeling is that multiple event handlers like this for dataserver() and friends would be better handled through a single event handler that does dispatch itself (probably through some wrapper API that we'll write eventually.)

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.

9 participants