JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Conversion Exploration

Aug 5, 2025 • [luamoverstesting]


This morning I plan to look at the main loop for one of our movers and see what it might be like in SLua. I’ll just follow my nose: I have no idea where this leads. Just a quick look.

The action I’m interested in takes place in the timer event:

timer() {
    if ( VehicleType == FollowerType ) {
        list details = llGetObjectDetails( EngineKey, [OBJECT_DESC]);
        DistanceAroundTrack  = llList2Float (details, 0) - CarOffset;
        update_position();
    } else { // ENGINE, TRAM, SHUNTER
        update_position();
        update_speed();
    }
    
    moveToTrackPosition();
}

We see some duplication here, just the two calls to update_position, and, of course there’s a conditional check taking place on every cycle. With SLua, functions are first-class values, and we might do something like this:

local vehicle_timer = nil
function initialize_timer()
    if VehicleType == FollowerType then
        vehicle_timer = follower_update
    else
        vehicle_timer = engine_update
    end
end

function follower_update()
    local details = ll.GetObjectDetails(etc)
    etc
    update_position()
end

function engine_update()
    update_position()
    update_speed()
end

function timer()
    vehicle_timer()
    moveToTrackPosition()
end

We have notably more code with the scheme above, but we have a simpler and faster timer function. I would agree with the objection that saving a function in a variable is kind of new and initially confusing, and I would offer the kind and thoughtful advice to Get Over It and to Catch Up With The Times!

I also note two conflicting coding standards here. Are we going to name functions in snake_case or camelCase? We need to make up our minds. Using both can only lead to errors and duplication. I believe that Roblox Luau recommends camelCase for variables and functions. Personally, I am currently used to snake_case but mostly I think we just need a standard.

Let’s look at moveToTrackPosition and its friends:

moveToTrackPosition() {
    // Check other llSetObjectDesc calls??
    if (RootPos == ZERO_VECTOR) return;
    if ( VehicleType == EngineType ) {
        llSetObjectDesc((string)DistanceAroundTrack);
    } 
    integer trackIndex = (integer)DistanceAroundTrack;
    checkTriggers(trackIndex);
    move_and_bank(trackIndex);
} 

move_and_bank(integer trackIndex) {
    // should check here that RootPos isn't zero
    float    fraction = DistanceAroundTrack - trackIndex;
    vector   prevNode = getTrackPosition(trackIndex-1);
    vector   currNode = getTrackPosition(trackIndex);
    vector   nextNode = getTrackPosition(trackIndex + 1);
    vector   movePos  = currNode + fraction*(nextNode-currNode) + <0,0,HeightAboveRails>;
    rotation trackRot = getTrackRotation(prevNode, currNode, nextNode, fraction);
    rotation bankRot  = calculateBankRotation(DistanceAroundTrack);
    rotation moveRot  = bankRot*trackRot; 
    llSetLinkPrimitiveParamsFast(link(), [PRIM_POSITION, movePos,    PRIM_ROTATION, moveRot]); 
}

vector getTrackPosition(integer nodeIndex) { 
    vector pos = (vector)llLinksetDataRead("datakey"+(string)within(nodeIndex,ActualTrackLength));; 
    return pos*Scale*RootRot + RootPos;
}

Not much in moveToTrackPosition itself, except for the mixing of code formats (again!).

There is a global variable DistanceAroundTrack, which, I happen to know, was updated in update_position. It is referenced as a global both in moveToTrackPosition and getTrackPosition. I also happen to know that check_triggers thinks it wants the integer part of DistanceAroundTrack.

I would argue that there should be far fewer functions looking at that global. Perhaps something like this:

function timer()
    vehicle_timer()
    moveToTrackPosition(DistanceAroundTrack)
end

function move_to_track_position(distance)
    if RootPos == ZERO_VECTOR then return end
    if VehicleType == EngineType then
        ll.SetObjectDesc(tostring(distance))
    end
    check_triggers(distance)
    move_and_bank(distance)
end

function move_and_bank(distance)
    local track_index = distance // 1
    local fraction = distance - track_index
    local prev_node, curr_node, next_node = get_track_positions(track_index)
    local move_pos = curr_node + fraction(next_node-curr_node) + HeightOffset
    local track_rot = get_track_rotation(prev_node, curr_node, next_node)
    local bank_rot = calculate_bank_rotation(distance)
    rotation move_rot = bank_rot * track_rot
    ll.SetLinkPrimitiveParamsFast(link(), etc)
end

function get_track_positions(node)
    return 
        get_track_position(node-1),
        get_track_position(node),
        get_track_position(node+1)
end

function get_track_position(node)
    local adjusted = within(node, ActualTrackLength)
    local vec_string = ll.LinkSetDataRead('datakey'..tostring(adjusted))
    return -- whatever code converts the string to a vector
    return 
end

move_and_bank appears to me to need some improvement. Certainly the three lines that compute move_rot could be extracted. I might explore extracting the three nodes prev, curr, next and then calling a function that uses them to compute position and rotation. The code there isn’t bad, and, within reason, we don’t want to call too many functions when we’re running on the timer.

Ah. No one uses track_index other than to get the nodes. We do need fraction however.

If I had a few decent tests for this code, I’d experiment with some refactoring. Again, it’s not bad … but my intuition says it could be better.

For example, I wonder whether a little “method object” for computing the position and rotation from three nodes might be a useful item. I’d want to have the code running under tests to figure that out.

I think that the next time I do this, I’ll actually rig up some testing. That will serve two purposes: it’ll surely help us learn how to test this code, and it will probably allow us to refactor it fearlessly to get it as tight and nice as we can.

An open question—just because I haven’t done it at all—is how we’ll convert our LSD notation “<123.45, 234.56, 345.67>” into an SLua vector. Casting won’t do it. Possibly there will be facilities in SLua vector, possibly we’ll have to code something. Either way not a big issue, just an issue.

Summary

Standard naming would help. There are names in camelCase, CapitalCamelCase, snake_case, LOUD_SNAKE_CASE, and probably more schemes. I presently favor snake_case, and as I mentioned, I think there are Luau recommendations in Roblox. I don’t know what scheme, if any, SL scripts will generally recommend. Maybe I’ll file a “canny” about it.

At this level of the code, I’m not seeing anything very thrilling in the conversion to SLua. I would like to think about some little objects that might be helpful, just because I’m an object-oriented kind of person, but in SLua, and our code here, there are reasons to imagine that we’d often do better with vanilla functions.

I think we could create code that is marginally better, and possibly marginally faster than LSL at this level. I’m thinking ten percent kind of improvement, not fifty. Other areas may provide more opportunity. Menus, for example, look to be easier to build with SLua facilities. And we might find other interesting code not so close to the timer event.

We’ll see. We’re just exploring the world, reporting on what we encounter.

Safe paths!

In order to decide what to do, I’ll continue to try things, learning good ways—and not so good ways—to do things.