Updated Wind Sculpt

I’ve been fiddling around with Linden Scripting Language for over nine years now.  It’s an interesting, quirky language, with a huge number of functions in its library (nearly 500).  While I think I know it pretty well, language and library features can always surprise even the most experienced coder.

A week or so ago, I found myself in the position of wanting to be able to rez and derez 64 linked objects.  Previously, I’ve done this by putting a script in the objects to be deleted that listened to the master script and then deleted themselves when told. But…

64 scripts, even though they were very small (about 5 lines of code and less than 6K of memory each), nearly doubled the land impact of my 65 piece object!  There had to be a better way.  And there is.

There are two functions called llSetRemoteScriptAccessPin() and llRemoteLoadScriptPin() that will allow you to load a script to an object on demand and set it to running.  I won’t give you an example here, you can see some at the links to the functions.  All this is leading up to the fact that I have an existing product on MP that also does rez and derez of multiple linked objects, and uses the old method.  Can we use these functions to drop its land impact?  No prize for a correct guess!

This is the BF Wind Sculpt, a kinetic wind sculpture based on Anthony Howe’s Di-Octo.  It rezzes and derezzes its vanes on demand.  With the old system with a script in each vane listening to the support to say “delete yourself”, the object weighs in at 13 LI.  Using the new method, it comes in at 10.  Yay!  If you own a copy, you should now have version V1.3.


Script memory

Thinking about script memory in LSL is very important.  In Mono, which is the modern script engine, each script gets 64K of memory allocated by default.  This is true even if the script uses only a few kilobytes of memory.

With thousands of scripts in a region, this can easily consume all the memory allocated to the region, and cause the region to start swapping memory out to disk (this all happens at the Linden Lab server running the region).  Swapping is a Bad Thing™ and will drastically slow down the sim, which you’ll see as lag.

So, what as script creators can we do?  If you have a script that takes no external input, you can limit the amount of memory your script will be allocated with a llSetMemoryLimit () function call.

Firstly, what do I mean by “takes no external input”?  An example of something that takes external input is something like a picture frame that displays the owner’s selection of textures.  The owner can drag textures into the frame and display them.  Typically to do this, the script will load the names of the textures into a list, and therefore the amount of memory used by the list is out of the control of the script creator.  It depends on how many textures the owner loads into the frame’s contents, and not how the script creator codes the script.

So, if you have something that doesn’t allocate memory out of your control, how do you know what upper limit to set with your call to llSetMemoryLimit ()?  There’s a set of memory profiling calls that will tell you.  And, as you may have guessed, this post was prompted by someone triggering the swapping problem on my home sim, which made me see if I could drop the memory that my scripts allocate.

To make things easy, I created an include file for my scripts.  Let me show you an example of how I use it.  You can access the included files by following the link in the comments next to them.

#define DEBUG
#include "debug.lsl"      // See the source of debug.lsl

#define MEMLIMIT 8000
#define PROFILE
#include "profile.lsl"    // See the source of profile.lsl

#define BLUE "c3623b1f-db83-4003-bb6d-d0d60d32c621"

phantom (integer p) {
    llSetLinkPrimitiveParamsFast (LINK_THIS, [PRIM_PHANTOM, p]);
}

default {
    state_entry () {
        init_profile ();
        llCollisionFilter ("", BLUE, TRUE);
        phantom (FALSE);
    }

    collision_start (integer n) {
        phantom (TRUE);
        llSetTimerEvent (2.0);
    }
    
    timer () {
        llSetTimerEvent (0.0);
        phantom (FALSE);
        show_profile ("timer");
    }
}

This is a simple script that goes in an invisible barrier.  It lets me walk through it by turning the barrier phantom when I collide with it, but for everyone else, it does nothing.  It takes no external input, all the memory it will use is its code, it doesn’t even use any variables!  So this is a perfect candidate for limiting script memory allocation.

The code at the top includes my standard debugging code, and the

#define MEMLIMIT 8000
#define PROFILE
#include "profile.lsl"

defines an initial memory limit of 8000 bytes, tells profile.lsl to enable profiling, and includes the profiling code.

Now notice the calls to init_profile () and show_profile ().  The init_profile () call sets the script’s memory limit based on the MEMLIMIT definition, and starts memory profiling if PROFILE is defined (and, just a warning here, memory profiling can drastically slow down your script so make sure you turn it off when done!)

The show_profile () call displays the current maximum amount of memory the script has used.  It can be tricky to figure out where to put this call, but in this case, it’s easy, as the script flow first sets everything up and then waits for a collision by me, followed by the timer firing.  So the logical place to put the show_profile () call is at the end of the timer.  All the code has run by this stage.

When you save this, and collide with it (if you’re trying this, don’t forget to replace my UUID with yours!), you’ll see something similar to the following…

Barrier: Memory profile init: 0
Barrier: Memory profile show (timer): 7598

This tells us that at the end of the timer event, the most memory the script has ever used is 7598 bytes, so our starting figure of 8000 was close.  If you see

Barrier: MEMLIMIT = 8000 too small

increase the size of MEMLIMIT until this message goes away.

I will leave a little buffer between the size reported and the limit, just in case 🙂  So in my example, I left MEMLIMIT at 8000.  Doing this simple exercise saved over 56K!  That may not sound like a lot, but don’t forget there can be thousands of scripts running in a region, so it adds up.

Once you’ve figured out a good number for MEMLIMIT, don’t forget to turn off debugging and profiling by changing the #define DEBUG and #define PROFILE to #undef DEBUG and #undef PROFILE, and leave the init_profile () and show_profile () calls in place.  The init_profile () call just sets the memory limit when PROFILE is undefined (and the show_profile () call is replaced by a blank line of code).

If every script creator did this, we’d be living much less laggy second lives!


Basic animation chooser

Sometimes, you just need something simple to pick an animation to play.  Here’s a script to do just that.  Rez a prim, drop this script in it, and a bunch of animations, then take and wear it as a HUD.

When you touch it, it will offer you a dialog where you can play any of the animations, or stop the one currently playing.

Note the links to the included scripts.  If you don’t use the Firestorm preprocessor, you can get the source for the #included bits by following the link in the comment next to the #include.  Just cut and paste the code into the top of this script, replacing the #include.

#include "dialog_plus.lsl"  // see the source of dialog_plus.lsl
#include "privchan.lsl"     // see the source of privchan.lsl

integer g_have_perms;
string g_current;
list g_animations;
key g_owner;
integer g_channel;
integer g_listen_handle;
string g_msg = "Select";

default {
    
    attach (key id) {
        if (id != NULL_KEY) {
            llResetScript ();
        } else {
            if (g_have_perms && g_current != "") {
                llStopAnimation (g_current);
            }
        }
    }

    state_entry () {
        integer n = llGetInventoryNumber (INVENTORY_ANIMATION);
        integer i;
        string name;
        g_current = "";
        g_animations = ["Stop"];
        g_have_perms = FALSE;
        g_owner = llGetOwner ();
        g_channel = privchan ();
        for (i = 0; i < n; i++) { 
            name = llGetInventoryName (INVENTORY_ANIMATION, i);
            if (llStringLength (name) > 24) {
                llOwnerSay ("Animation \"" + name + "\" longer than 24 chars, not loaded");
            } else {
                g_animations += name;
            }
        }
        llRequestPermissions (g_owner, PERMISSION_TRIGGER_ANIMATION);
    }
            
    run_time_permissions (integer p) {
        if (p & PERMISSION_TRIGGER_ANIMATION) {
            g_have_perms = TRUE;
        }
    }
    
    touch_start(integer total_number) {
        llListenRemove (g_listen_handle);
        g_listen_handle = llListen (g_channel, "", g_owner, "");
        DialogPlus (g_owner, g_msg, g_animations, g_channel, g_menu_idx = 0);
    }

    listen (integer chan, string name, key id, string data) {
        if (chan == g_channel) {
            if (data == "Back") {
                DialogPlus (g_owner, g_msg, g_animations, g_channel, --g_menu_idx);
            } else if (data == "Next") {
                DialogPlus (g_owner, g_msg, g_animations, g_channel, ++g_menu_idx);
            } else if (data == "Stop") {
                if (g_current != "" && g_have_perms) {
                    llListenRemove (g_listen_handle);
                    llStopAnimation (g_current);
                    g_current = "";
                }
            } else {
                if (g_have_perms) {
                    llListenRemove (g_listen_handle);
                    if (g_current != "") {
                        llStopAnimation (g_current);
                    }
                    g_current = data;
                    llStartAnimation (g_current);
                }
            }
        }
    }
}

Detecting RLV

As you may know, I love scripting.  Last Halloween, I made up some RLV traps to scatter around the build for the unwary.  One was a giant skeletal hand that popped out of the ground and grabbed the unsuspecting victim hehehe.

I though you might be interested to see how the basis of RLV scripting works.  Feel free to skip this article if you have zero script interest 😉

To be affected by these type of scripts, avatars must wear an RLV relay and be using an RLV capable viewer, for example, Firestorm.  The relay acts as a bridge between the scripted object and the avatar’s viewer, receiving commands and then those commands are intercepted by the viewer.  This is how RLV can control your viewer.

The RLV relay listens on a specific channel (you can find heaps of detail about all this in the RLV Relay Protocol document).  The first thing you generally do is find a list of avatars wearing an active relay so you can perform some action on them.  That’s what this example script does, find a list of RLV enabled avatars.

#define DEBUG
#include "debug.lsl"        // See the source of debug.lsl
#define TIMEOUT 1.0         // #define is part of the Firestorm preprocessor
#define CHAN 12345          // You can just define these as normal variables
#define RLVCHAN -1812221819 // if you're not using Firestorm

list g_avis;
key g_target;
integer g_idx;
integer g_handle;
string g_command;

default {
    state_entry () {
        debug ("state default");        
    }

    touch_end (integer n) {
        llOwnerSay ("Checking for active RLVs...");
        g_avis = llGetAgentList (AGENT_LIST_PARCEL, []);
        state rlv;
    }
}

state rlv {
    
    state_entry () {
        if (llGetListLength (g_avis) > 0) {
            g_target = llList2Key (g_avis, 0);
            llListenRemove (g_handle);
            g_handle = llListen (CHAN, "", "", "");
            g_command = "testing";
            llRegionSayTo (g_target, RLVCHAN, 
                           g_command + "," + (string)g_target + ",@versionnum=" +
                           (string)CHAN);
            llSetTimerEvent(TIMEOUT);
        } else {
            llOwnerSay ("--");
            state default;
        }
    }
    
    listen(integer channel, string name, key id, string message) {
        if (llGetOwnerKey (id) != g_target) {
            return;
        }
        if (g_command == "testing") {
            llSetTimerEvent (0.0);
            llListenRemove (g_handle);
            llOwnerSay (llGetDisplayName (g_target));
            g_avis = llDeleteSubList (g_avis, 0, 0);
            state redo_rlv;
        }
    }
    
    timer() {
        llSetTimerEvent (0.0);
        llListenRemove (g_handle);
        g_avis = llDeleteSubList (g_avis, 0, 0);
        state redo_rlv;
    }    
}

state redo_rlv {
    state_entry () {
        state rlv;
    }
}

The basis of this script is the llRegionSayTo () function, the listen, and the timer in the rlv state.  Saying a string to an avatar formatted correctly (in this case, we are asking for the avatar to tell us the version of RLV they are running) will cause the avatar to send us a string back with the information, which is received by the listen.  In the event the avatar does not have a relay on, they won’t respond with anything, so eventually, we remove the listen with a timer.


Script preprocessing

As you may know if you’ve read my “About Blue” page, I love writing scripts to get objects to do things in-world.  It’s one of the reasons I joined SL, to discover what I could do with a new and interesting programming language.  So, you get the occasional technical article on the blog.  Feel free to skip this if you have no interesting in scripting 🙂

LSL is limited is a number of ways.  One of them that it lacks a preprocessor.  Preprocessors allow you to do things like:


#define CONSTANT value

and wherever the word CONSTANT appears in the script, the preprocessor will substitute value before it compiles the resulting code.  This is great for things like debugging scripts, and many other uses.

If you are using Firestorm however, you can switch on a preprocessor for LSL built into the viewer!

Other than doing substitutions such as the example above, there are a number of things you can use defined constants for, such as conditional compilation. Also, the preprocessor implements a number of other functions such as code inclusion.

The feature I want to talk about though is the switch statement.  LSL lacks this common feature of other C and Java-like languages.  So the Firestorm developers included a way to have the preprocessor do it.

In their basic form, switch statements look like:


switch (expression) {
    case value1: {
        do something;
    }
    case value2: {
        do something else;
    }
}

And here is the point.  We know that expression is something that can return an integer.  The first time I used a switch statement in LSL, I coded what any C or Java programmer would and said:


switch (llListFindList (list1, list2)) {

knowing that llListFindList () would return an integer result.  But on looking at the resultant code (you can see this in the preprocessor window in Firestorm after your code is preprocessed), I noted that the switch statement is translated into multiple if statements with jumps, but the preprocessor is not smart enough to evaluate the expression once and use the result!  So if you have 40 case statements, the resulting code will have 40 calls to llListFindList () !  This is terrible for performance!

If you are using switch statements in your LSL, please evaluate your expression before handing it to the preprocessor.  For example:


integer result = llListFindList (list1, list2);
switch (result) {

This will ensure expression is only evaluated once.

You can read more about the Firestorm preprocessor here.


Butterflies, part 2

Yesterday we talked about a script to add to some cheap copy mod butterflies available on the marketplace to prepare them for use in a rezzer.  Today, we’ll look at the rezzer script and how to put it all together.

Let’s jump in and look at the rezzer script right away.  All we are going to have to do is place this, an optional notecard, and our modified butterflies in a prim, and we’ll be ready to go!  Here’s the script (don’t panic, it’s a little larger than yesterdays!):

 

integer g_rezzed;
integer g_chan = -88503;
integer g_nc_line;
integer g_rez_count;
integer g_pos_count;
integer g_obj_count;
key g_nc_id;
list g_positions;
list g_objects;
string g_nc_name = "config";

default {
    
    on_rez (integer n) {
        llResetScript ();
    }
    
    changed (integer c) {
        if (c & CHANGED_INVENTORY || c & CHANGED_REGION_START) {
            llResetScript ();
        }
    }
    
    state_entry () {
        llRegionSay (g_chan, "SWAT!");
        integer i;
        g_objects = [];
        g_positions = [];
        g_pos_count = 0;
        string name;
        integer n = llGetInventoryNumber (INVENTORY_OBJECT);
        for (i = 0; i < n; i++) { 
            name = llGetInventoryName (INVENTORY_OBJECT, i);
            if (llGetSubString (name, 0, -2) == "butterflys") {
                g_objects += name;
            }
        }
        g_obj_count = llGetListLength (g_objects);
        if (g_obj_count == 0) {
            llOwnerSay ("There are no butterflys objects in the inventory!");
            return;
        }
        g_rezzed = FALSE;
        g_nc_line = 0;
        if (llGetInventoryType (g_nc_name) == INVENTORY_NOTECARD) {
            g_nc_id = llGetNotecardLine (g_nc_name, g_nc_line);
        } else {
            llSetTimerEvent (30.0);
        }
    }

    dataserver (key id, string data) {
        if (id == g_nc_id) {
            if (data != EOF) {
                if (llGetSubString (data, 0, 0) != "#") {
                    g_positions += (vector)data;
                    llRegionSay (g_chan + g_nc_line, "SWAT!");
                }
                g_nc_id = llGetNotecardLine (g_nc_name, ++g_nc_line);
            } else {
                g_pos_count = llGetListLength (g_positions);
                llSetTimerEvent (30.0);
            }
        }
    }

    timer () {
        vector sun = llGetSunDirection ();
        if (sun.z > 0) {
            if (g_rezzed == FALSE) {
                integer i;
                for (i = 0; i < g_pos_count + 1; i++) {
                    llRezObject (llList2String (g_objects, (integer)llFrand (g_obj_count)),
                                 llGetPos () + <0.0, 0.0, 1.5>,
                                 ZERO_VECTOR,
                                 ZERO_ROTATION,
                                 g_chan + i);
                    g_rez_count++;
                }
                g_rezzed = TRUE;
            }
        } else {
            if (g_rezzed == TRUE) {
                integer i;
                for (i = 0; i < g_pos_count + 1; i++) {
                    llRegionSay (g_chan + i, "SWAT!");
                }
                g_rezzed = FALSE;
            }
        }
    }

    object_rez (key id) {
        if (--g_rez_count > 0) {
            llSay (g_chan + g_rez_count - 1,
                   (string)llList2Vector (g_positions, g_rez_count - 1));
        }
    }
    
    touch_start (integer n) {
        if (g_rezzed == TRUE) {
            integer i;
            for (i = 0; i < g_pos_count + 1; i++) {
                llRegionSay (g_chan + i, "SWAT!");
            }
            g_rezzed = FALSE;
        }
    }
}

 

OMG you might be saying 🙂 Let’s take it slow, and go through this. I’ll describe each event handler, so hopefully you can grasp what’s going on.

  • on_rez – we are just restarting the script.
  • changed – we restart the script on a region restart so as to make sure we either rez or derez the butterflies.  We also restart if the inventory changes, which makes it easy to add lines to the notecard for additional butterflies .
  • state_entry – we initialize some variables, find the names of the butterfly objects in our inventory (remember, I said yesterday to name the butterfly objects “butterflys1”, “butterflys2”, and “butterflys3”?).  Then we start reading our notecard, which will transfer control to…
  • dataserver – which gets each line in our notecard, and stores the data in a variable called g_positions, and once we hit the end of the notecard, we start a 30 second timer.  We also derez any existing butterfly objects.
  • timer – here’s where all the work gets done.  Every 30 seconds, we check if the sun is up (indicated by the Z value of the sun’s position).  If the sun is up, and we haven’t rezzed any butterflies yet, rez a randomly selected butterfly object from our inventory 1.5 metres above the rezzer for each of the positions in our notecard.  If the sun is not up, say “SWAT!” to each of our rezzed butterflies on the channel they are listening on to delete them.  Note the last parameter of the llRezObject call.  It tells each butterfly object what channel to listen on (the parameter in the on_rez event of yesterday’s script).  Also note that the rez loop rezzes one additional butterfly object and leaves it above the rezzer.   This makes it easy if you just want one butterfly set.
  • object_rez – here we tell each newly rezzed butterfly object the position it should move to.
  • touch_start – just in case you need to reset everything, you can touch the rezzer to delete any existing butterflies.

We need an optional notecard in the rezzer’s contents called “config”.  It consists of lines containing positions in region co-ordinates where you want additional butterflies to be.  You can also comment out a line by putting a hash in the first column.  So you might have something like this:

# butterflies for woods
<104.3, 10.5, 26.0>
# for skybox
<218.5, 132.5, 2026.0>

After all that we better have a picture 🙂

Here’s a screen shot of the rezzer with the config notecard, the above script, and the butterfly objects in its contents:

Here I’ve made the rezzer white so you can see it, but you should set it to 100% transparent, and phantom too.  You also must call the rezzer “Butterfly rezzer”, because that’s what our script from yesterday is expecting.

And that’s it!  If you’ve followed along with the instructions, 30 seconds after you drop the script in the rezzer, your butterflies should rez!  Assuming the sun is up 🙂  Take the rezzer into your inventory and position where you want one set of butterflies to be.  Add lines to the config notecard for additional ones!  Have fun!


Butterflies, part 1

Butterflies don’t fly at night, have you noticed?  Lots of builders just rez some and leave it at that.  Let’s see if we can get some realism.

I’ll wait while you go and buy some butterflies to play with from this marketplace listing.  They’re only L$10…

Got them?  Good.  Once you unbox them you will see they are copy, modify, no transfer.  This is exactly what we want as we can add scripts to them and rez them to our hearts content.

There are three different types in the box, and we are going to rez them one by one, add a script to them, and take them back into our inventory with a different name so we know which ones are ours.

Here’s the script we are going to add to them:

integer g_chan;

default {

    on_rez (integer chan) {
        g_chan = chan;
        llListen (chan, "Butterfly rezzer", NULL_KEY, "");
    }

    listen (integer chan, string name, key id, string msg) {
        if (chan == g_chan) {
            if (msg == "SWAT!") {
                llDie ();
            } else {
                llSetRegionPos ((vector)msg);
            }
        }
    }
}

Let’s examine what this does.  On rez, we get the integer that our rezzer passed us and listen to the rezzer on that channel.  When we receive a message from the rezzer, we delete ourselves if the message is “SWAT!”, else we interpret the message as a position in region co-ordinates and set our position there.  Pretty straightforward, right?

Create a new script in your inventory (probably in the folder you have the butterflies in) and call it something like “Butterfly listener”.  Then, rez each butterfly object, edit it, and drag the script into it.  Editing these can be a little tricky, so use area search to find and edit them.

Here’s a screen shot showing one set of butterflies rezzed with the new script in them.

Name the butterfly objects “butterflys1”, “butterflys2”, and “butterflys3” either while you’re editing them or when you take them back into your inventory.  Note I’ve chosen to keep the creator’s spelling mistake as this makes them easier to find lol.  Note that all lower case is important!  If you don’t name them exactly this, tomorrow’s script won’t work.  You can see them in my inventory window in the above photo.

Tomorrow, we’ll examine the rezzer script, the control notecard, and how to put it all together.  Say tuned!