Category Archives: Scripthackery

All about posts dealing with Linden Scripting Language (LSL), possibly including some sample code.

A Scripted Bagatelle

This evening, Lexx handed me a small object made by a fellow named Petr Brandenburg, that alternately flashed yellow and cyan. She couldn’t read the script inside it, so she didn’t know how it worked. She asked if I could figure it out.

Within a few minutes, I had handed her this:

// Neon Flasher Script
// Erbo Evans - 3/23/2008
/////////////////////////////////

// List of colors to cycle through, specified as  tuples.
list colors = [<0.0,1.0,1.0>, <1.0,1.0,0.0>];
float delay = 0.5;  // how long to wait between color changes
integer index;  // counter used to keep track of color state

default
{
    state_entry()
    { // initialize the color index and display the first color
        index = 0;
        llSetColor(llList2Vector(colors,0),ALL_SIDES);

        // set up the timer to flash colors
        llSetTimerEvent(delay);
    } // end state_entry

    timer()
    { // advance to next color and display it
        if (++index>=llGetListLength(colors))
            index = 0;  // wrap around back to beginning if we run off the end
        llSetColor(llList2Vector(colors,index),ALL_SIDES);
    } // end timer
} // end state default

Simple enough to understand. The script itself is basically a timer-driven state machine, using a table (the colors list) to specify the colors to be displayed on the prim (which uses a blank texture on all faces to better show the effect).

As an exercise, I asked her, “How would you extend the script to flash more than two colors?” Because of the way I wrote the code, it’s very easy (just extend the colors list). However, she stared at it and felt her brain begin to melt. I think it was mostly due to the fact that she was running on only four hours sleep, though. 🙂

Advertisements

1 Comment

Filed under Scripthackery

Notecard Writing: A Modest Proposal

Jacek laments the lack of ability of scripts to write to notecards, thus depriving them of a potentially-useful mechanism for saving persistent data across script resets. (Other scripters have used the object name or description to store such persistent data, but the fact that LL is now enforcing strict limits on the length of those fields limits the usability of this technique.) We are told that this is because each edit to a notecard creates a new asset in the asset server, and it would possibly overwhelm the server, as if it wasn’t overwhelmed enough already.

Well, how does it handle people editing notecards normally? I’m guessing that, when you bring up a notecard window in the client, you don’t actually create a “new” asset until you press the Save button. Is there any reason why we couldn’t have script APIs that work the same way? Say, when you open a notecard for writing, buffer the data in memory, and only actually save that data, creating the new asset, when the notecard is closed?

Here’s an example of how these APIs might look. (Warning! These are not real APIs! I’m only showing you what I think they might look like.)

integer llNoteStreamOpen(string name, integer mode);

Opens a notecard for writing. name specifies the name or key of the notecard to be written to, which must be an existing notecard in the inventory of the object holding the current script. mode must be one of NOTESTREAM_OPEN_OVERWRITE (to overwrite the existing data in the notecard) or NOTESTREAM_OPEN_APPEND (to append to the existing data in the notecard). The return value is a “handle” used to interact with this notecard stream, or -1 on error.

Scripts may only have a maximum of N notecard streams open at one time (where N is a small number, perhaps as low as 1). Attempts to open additional streams beyond that result in an error.

integer llNoteStreamWrite(integer handle, string data);

Writes data to the notecard stream. Data written is buffered and is not actually saved to the notecard until the stream is closed. handle specifies the handle of the notecard stream to write to. data specifies the string data to be written. The return value is the number of characters written to the notecard stream, or -1 on error.

key llNoteStreamClose(integer handle, integer disposition);

Closes a notecard stream. handle specifies the stream to be closed. disposition must be either NOTESTREAM_SAVE (to save the changes to the notecard) or NOTESTREAM_DISCARD (to discard the changes to the notecard). Returns the UUID of the notecard that was modified (the new UUID if changes were saved, the original UUID if changes were discarded); returns NULL_KEY on error.

When a script is reset, any notecard stream handles it holds open are closed as if llNoteStreamClose(handle,NOTESTREAM_DISCARD) were called on them.

I don’t know how feasible this all would be to implement. Perhaps we’d need an upper limit on the buffer size for the notecard stream, which would limit its usability for some purposes, but not for others. Perhaps any or all of these functions would need script execution delays built into them. Perhaps the streams should be automatically closed/discarded when the script changes states, much the same way timer handles get reset, instead of when the whole script is reset.

Thoughts?

1 Comment

Filed under Scripthackery, Technical

A Venue Submitted For Your Consideration

It would seem that the esteemed Ms. Malaprop, Script-Creator Extraordinaire, has grown frustrated to some degree with (what’s left of) the original Second Life Forums, at least for purposes of discussion of scripting, including the posting of properly formatted example code.  As befitting the manner in which programmers are often moved to start projects–by “scratching an itch,” or fulfilling a personal need–she has opened Ordinal’s Scripting Colloquium, a venue for the discussion of scripting, which includes proper formatting (BBCode and Markdown) to allow script text to be printed such that it looks correct.  All Residents with an interest in scripting are encouraged to visit it post haste and forthwith.  (I certainly plan on doing so!)

Leave a comment

Filed under Scripthackery

Towards Mark II

Erbosoft Distributed Music Changer AdI’ve somehow managed to sell a number of copies of the Erbosoft Distributed Music Changer, Mk.I, and, of course, I’ve been using it at the Gin Rummy and the Evans Family Compound for awhile now. However, I’m brainstorming ideas now which should eventually lead to an updated version, if I can find the time to put them into actual code. Here’s some of the things I’m thinking of:

  • I really need to use the new llRegionSay() API with the thing, to eliminate the 100-meter limitation and let you use terminals anywhere on the same region as the master component. However, there’ll have to be some way to allow messages to “straddle” region boundaries, too, so llShout() will have to stay in some form.
  • Multipage dialogs! I’m sure people might have more than 12 stations they want to use with their music changer…particularly clubs that have a lot of DJs. The terminal code will need to handle the presentation of multiple “pages.”
  • Some way of “interlinking” two Master Components, so they share station lists and commands. This is how I might handle the region boundary issue on messages, too, by making the interlink protocol use llShout() rather than llRegionSay().
  • Dynamic configuration of the system channel. We need to have some form of “discovery” so that Master Components can pick the channel on which they will communicate with terminals and slave tuners, and give terminals a way of “rendezvousing” with the Master Component dynamically. (How do we identify which “system” a terminal should consider itself part of? Based on what? Object ownership? Group membership?  And Slave Tuner Components present their own unique problems.)
  • Some sort of dialog-based configuration override on the Master Component side, to allow configurations to be changed on the fly.
  • A list-based access control system.
  • And lastly, and most complicated…Figure out how to extract the current song information (artist & title) from the MP3 stream! I’ve been tearing my hair out looking for information online that would tell me how this is done…hopefully I won’t have to grovel over the source of, say, xmms to figure out how.

Of course, if the RIAA gets their way, there may not be many Internet music sources left to use with a music-changer device.  Which would suck for us in ways that go far beyond a simple L$500 box.

Leave a comment

Filed under Business, Scripthackery

Practical Applications of the Update

It’s an ill wind that doesn’t blow some good…and one of the new API functions added in 1.13 has inspired a quick hack from me that is both ingenious and helpful.

We have a private build-pad floating high above the Evans Family Compound now (high enough to conform with Fantasyland regulations, of course), and I’ve been letting Allie borrow it to work on some of her creations. It occurred to me that she might want to keep track of how many prims her creations were using…and, to that end, I invented a “prim scale.”

// Prim Scale Script
// Erbo Evans - 12/3/2006
///////////////////////////////////////////////////////

integer s_tare = 0;
vector s_pos;

display()
{
    integer count = llGetParcelPrimCount(s_pos,PARCEL_COUNT_TOTAL,FALSE);
    integer real = count - s_tare;
    llSetText("Prim count: " + (string)real + "\nClick to set tare.",<1,1,1>,1.0);
}

default
{
    state_entry()
    {
        s_pos = llGetPos();
        display();
        llSetTimerEvent(10.0);
    }

    touch_start(integer total_number)
    {
        s_tare = llGetParcelPrimCount(s_pos,PARCEL_COUNT_TOTAL,FALSE);
        display();
    }

    timer()
    {
        display();
    }
}

This displays a running count of the total prims being used on the parcel, and, if you click it, sets the current prim count as a “tare” amount, so that one may click on the “scale” object, then start building, and have an accurate running count of the number of prims created since the “tare” was set. The display is updated every 10 seconds.

The display work is pretty much all done in the display() routine, which is called by all three event handlers. The key is the use of the new llGetParcelPrimCount() API, which returns the number of prims (of various types, depending on the second argument) on the parcel at the location given by the first argument. We use the value of llGetPos() for that first argument, to specify “the parcel where I am right now.” The third argument is the “sim-wide” flag, which obviously we want as FALSE. The “tare” value, which is used to bias the output, is stored as s_tare, initialized to 0 and set in the touch_start() handler.

Obviously, this is very simple, and there are a few improvements I could think of yet…but even this device makes a nice little tool for keeping track of how many prims you’re using in a build.

2 Comments

Filed under Scripthackery

Adventures In Applications

It’s been awhile since I posted any good LSL examples to this blog, so I’ll give you a fairly complicated one. Danielle wanted a kiosk in front of the Gin Rummy that would hand out “job application” notecards for any one of a number of positions. I determined that a dialog-based interface would work for that, and decided to make it configurable. So here’s the code:

// Gin Rummy Application Kiosk Script
// Erbo Evans 10/2/2006
//=============================================
// To set this up you will need to create a notecard named "_config"
// in the same object as this script.  This notecard will contain pairs of lines.
// The first line is the button label, and the second line is the name of the
// notecard to give out when the button is pressed.  Example:
//
// DJ
// DJ Application
// Host
// Host Application
// Bartender
// Bartender Application
//
integer MY_CHANNEL = -897414;
string PROMPT = "Thank you for your interest in employment at the Gin Rummy. Please select the position you are interested in:";

list s_master = [];
list s_buttons = [];
integer s_line_count;
integer s_card_count;
integer s_cur;
key s_qid;

default
{
    state_entry()
    {
        s_qid = llGetNumberOfNotecardLines("_config");
    }
    dataserver(key qid, string data)
    {
        if (qid!=s_qid)
            return;
        s_line_count = (integer)data;
        state load2;
    }
}

state load2
{
    state_entry()
    {
        s_cur = 0;
        s_qid = llGetNotecardLine("_config",0);
    }
    dataserver(key qid, string data)
    {
        if (qid!=s_qid)
            return;
        s_master += [data];
        if (++s_cur==s_line_count)
        {
            s_card_count = s_line_count / 2;
            integer i = 0;
            do
            {
                string s = llList2String(s_master,2 * i);
                s_buttons += [s];
            } while (++i<s_card_count);
            state runtime;
        }
        s_qid = llGetNotecardLine("_config",s_cur);
    }
}

state runtime
{
    state_entry()
    {
        llListen(MY_CHANNEL,"",NULL_KEY,"");
    }
    touch_start(integer n)
    {
        integer i = 0;
        do
        {
            llDialog(llDetectedKey(i),PROMPT,s_buttons,MY_CHANNEL);
        } while (++i<n);
    }
    listen(integer chan, string name, key id, string message)
    {
        if (chan!=MY_CHANNEL)
            return;
        integer i;
        do
        {
            integer n = 2 * i;
            string s = llList2String(s_master,n);
            if (s==message)
            {
                s = llList2String(s_master,n + 1);
                llGiveInventory(id,s);
                return;
            }
        } while (++i<s_card_count);
    }
}

In order for this to work, you need to have a notecard called _config in the object inventory along with the script. This notecard should contain, on alternating lines, the name to put on a button and the name of the notecard that will be delivered when you hit that button. Remember that llDialog allows you to have up to 12 buttons.

The default and load2 states are all about loading the number of lines of text in the notecard and the notecard’s actual data; the contents of the notecard are stored in the s_master list. Once all the notecard data has been loaded, the load2 state then strips out just the button names to add to the s_buttons list, which is used by llDialog. We could recompute this array on the fly every time we throw up a dialog, but doing it this way saves some time at the cost of a little extra memory, which we can afford.

After the lists are all full, the code clicks to the runtime state, which immediately starts listening on a private channel. (Channels with a number less than 0 cannot be triggered with the “/### message” syntax in chat, making them good for internal script uses like this.) Then it’s just a matter of throwing up the dialog box whenever the kiosk is touched, and responding to a message from the dialog, i.e. the name of the button that was pressed, by linearly searching the s_master list for the right button name and using the corresponding object name as an argument to llGiveInventory.

So, what could be improved in this? Well, one thing that a lot of people do with dialog boxes is that they don’t set up the actual llListen to the chat channel until the dialog box is summoned, and make that listen go away when a button is pressed, or after a timeout interval. The reason for doing it this way is to eliminate the tiny bit of lag caused by the listen. However, this comes at a slight cost of usability (since a dialog box may “time out” if a user hasn’t made a selection within the time limit), which is not generally a good thing in an object designed to interact with newbies. Plus there simply won’t be that much lag induced by listening on a single non-0 channel, and a private one at that. I could also rig the script to be sensitive to inventory changes, so that, when the configuration is changed, it could click back to the default state and reinitialize itself. But we don’t change the contents all that often (if ever), and we can just reset the script if it comes to that.

Share and enjoy! 🙂

Leave a comment

Filed under Scripthackery

Scripthackery: Improvements On A Theme

When establishing the photo studio in one corner of Don’t Panic!, Danielle set up a backdrop prim with a number of different textures in it for picture backdrops. To switch between them, she came up with this little script:

default
{
  touch_start(integer total_number)
  {
    integer number = llGetInventoryNumber(INVENTORY_TEXTURE);
    float rand = llFrand(number);
    integer choice = (integer)rand;
    string name = llGetInventoryName(INVENTORY_TEXTURE,choice);
    if (name!="")
      llSetTexture(name,ALL_SIDES);
  }
}

When touched, this script finds the number of textures the object contains in inventory, then picks a random number between 0 and that number minus 1. (As in most programming languages, casting from float to integer in LSL truncates everything after the decimal point.) Then it gets the name of the texture with that index and applies it to all sides. Simple enough.

Danielle wanted the script to say which texture was being displayed at touch time, so she asked me to take a look at the code and improve it. At the same time, I felt that random switching was not the way to go here; better yet would be sequential switching, to run through all the textures in order. Herewith my solution:

// Enhanced Texture Switcher
// Erbo Evans - 8/20/2006
///////////////////////////////////

list all_textures = [];
integer count = 0;
integer current = 0;

set_texture(integer ndx)
{
  string s = llList2String(all_textures,ndx);
  llSetTexture(s,ALL_SIDES);
  string p = (string)(ndx + 1);
  llWhisper(0,"Now displaying texture: " + s + " (" + p + "/"
              + (string)count + ")");
}

default
{
  state_entry()
  {
    count = llGetInventoryNumber(INVENTORY_TEXTURE);
    integer i;
    for (i=0; i<count; i++)
    {
      string s = llGetInventoryName(INVENTORY_TEXTURE,i);
      all_textures += [s];
    }
    llWhisper(0,"Texture switcher is ready.");
    set_texture(current);
  }

  on_rez(integer param)
  {
    llResetScript();
  }

  touch_start(integer total_number)
  {
    if (++current==count)
      current = 0;
    set_texture(current);
  }
}

The code now loads the names of all textures into a list at state_entry() time, for faster access. Notice that, after we load the name of a texture into the string variable s, we turn that into a single-element list to concatenate it onto the all_textures list. We also save the number of textures in count, to avoid having to call llGetListLength() unnecessarily later.

Since setting textures now involves a few different operations, I moved the process into its own function, set_texture(), which takes as an argument the 0-based index of the texture to display. Note that, when the texture number is actually displayed via llWhisper(), it is displayed as a 1-based index, which is more “natural” for humans to deal with. (Unless they’re programmers…but let’s not go there.)

The first two lines of the touch_start() handler will be difficult to understand for anyone not already familiar with LSL, or familiar with one of the languages that inspired it (C, C++, Java). The expression adds one to the current variable, then, if that variable is equal to the count, resets it to 0. “++” in this context is used as a pre-increment operator, which increments the value of current before using it in the test for equality with count. If I had written “current++” rather than “++current”, the “++” would have been a post-increment operator, which means the value of current would have been incremented after its use in the comparison with count. This is sometimes useful, too…just not here. (Sometimes it doesn’t matter which you use, as in the third expression of the for loop in the state_entry() handler. In those contexts, I tend to use the post-increment form. Others will use the pre-increment form here. That’s just a stylistic difference.)

The main drawback of this code as compared to Danielle’s is that it won’t automatically adapt to the addition of new textures to the object; you have to reset the script manually. I could get around that by adding a changed() handler to listen for inventory changes, and call llResetScript() when they happen. In actual practice, though, we don’t add new textures to the backdrop often enough to worry about this.

This code also worked well for an automatic picture cycler that displays a number of pictures I took of Danielle. I simply changed the touch_start() handler into a timer() handler, and called llSetTimerEvent() at the end of the state_entry() handler, with an appropriate delay value.

Share and enjoy! 🙂

5 Comments

Filed under Scripthackery