Configuring LSL Scripts Using a Notecard
Introduction
When building projects in Second Life, eventually you will find that some aspect of the system you design needs to be configurable by the user. Perhaps you need to limit access to a list of users, set a starting volume for a sound, or set a communication channel for multiple prims to talk to each other (without overlapping with someone else who uses your script on an adjacent parcel). In all these cases you can use a notecard-based system to configure your script.
In this article I’ll describe one approach to notecard-based configuration, using real-world example code. The material covered will require a basic understanding of LSL scripting.
Script Review
The first step when implementing a configuration notecard is to look at your existing code and try to identify values that need to be user configurable. Look for both variables and hard-coded values.
Let’s use the following script from a basic waterfall as an example:
-
default
-
{
-
{
-
}
-
}
When looking at this script we can see two potential values that could be user-configurable. The first value is the volume of the “water_running” sound: not all users are going to want the same volume depending on the size of their plot and their desire for the sound to travel. The second value that is a candidate for configuration is the rate of the waterfall animation: some users may want a faster or slower-running waterfall.
Creating the Notecard
We want it to be very easy for our users to understand the configuration notecard and make changes to it. To help make our notecards easy to understand, we support the use of comments. We keep things easy to edit by using a simple syntax of name = value.
Create a new notecard and rename it Configuration, then place the following in the notecard and place the notecard in the prim’s inventory:
# THIS IS THE CONFIGURATION CARD. YOU CAN COMMENT OUT LINES WITH THE # SYMBOL. # BLANK LINES ARE ALLOWED, AND SPACES ARE ALLOWED BETWEEN THE NAME, EQUAL SIGN AND VALUE# THE VOLUME OF THE WATERFALL, VALUE NEEDS TO BE BETWEEN 0 AND 1, WITH 1 BEING FULL VOLUME # AND 0.5 BEING HALF VOLUME. THE HIGHER THE VOLUME, THE FURTHER AWAY YOU CAN BE AND STILL # HEAR THE WATERFALL. volume = 0.5 # THE SPEED OF THE WATERFALL ANIMATION, VALUE INDICATES CYCLES PER SECOND. # THE DEFAULT SHOULD BE FINE FOR MOST, CHANGE IN SMALL INCREMENTS IF YOU WANT # A FASTER OR SLOWER WATERFALL. speed = 0.05
The heavy use of comments keeps the notecard easy to understand and easy to read. I personally use all-caps for the comments to make it easy to tell the difference between comments and values. Using spaces between the name, equal sign and value helps the user read the configuration.
Parsing the Notecard
Now that we’ve identified our configurable values and created our notecard, we need to modify our code to use the notecard during script initialization.
The first thing we need to do is break our script into two states: one for initialization and one for operation. I personally use the default state for initialization and then move to a second state for the regular running of my script.
We also add global variables to our script for all the user-configurable values and to identify our configuration notecard.
Here’s an example of the previous script with separate states and global variables:
Note that we initialize the variables with default values just in case the configuration notecard is either missing or has no setting for a given value. We still need to fill in the default state, which in turn will call the Running state.
First we need to add two additional global variables for keeping track of which line we are reading in the notecard, and to read the notecard:
Next, we use the state_entry() event to inform the user that we are initializing the script, check whether the configuration notecard is present and read the first line of the notecard:
The llGetNotecardLine() function is going to request the first line of the notecard specified and return it as a dataserver() event, identified by the QueryID that we captured. The next block of code we need will capture the event and process the notecard line:
First thing we do is check whether the QueryID matched with the one we captured when issuing llGetNotecardLine(), if it does not match we ignore the data since it likely relates to a different function call that triggered the dataserver() event (this should not happen since the llGetNotecardLine() function is the only thing triggering a dataserver event in this state, but checking the QueryID is a best practice).
Next we check whether we got an EOF instead of an actual notecard line. If we did, we know that we’ve run out of lines and we may as well move into the running state.
Finally, we add a state_exit() event to signal to the user that processing is complete.
Now we add the code that actually does the work of processing the notecard lines:
First we add some variables to our event code to store the values while we work on them. Next, we check whether the notecard line starts with a “#” character, denoting the line as a comment that does not need parsing, or is blank. In either case we skip the remaining parsing code.
Now that we know we are working with a line that is neither blank nor a comment, we split the line into two parts with the llParseString2List() function, using the equals sign as a delimiter. Next we pull the name and value from the list using llList2String() and trim them using llStringTrim(). We are then left with a name and a value.
We walk through the possible names using a series of “if”/”else if” statements, checking values and making assignments. Since all values are strings, we may need to cast them to the appropriate type for our global variables.
Once we’ve parsed the line (or skipped parsing), we increment the NotecardLine variable to work on the next line and call llGetNotecardLine() to start the process over again, repeating until we reach the end of the notecard and encounter an EOF.
Handling Notecard Changes
There are three ways to handle a user making changes to the notecard:
- Instruct the user to open the script and click the Reset button at the bottom of the editor.
- Decide on a trigger (such as a dialog button) and either go back to the initialization state or call the llResetScript() function in response.
- Catch the changed() event when the user saves their changes and either go back to the initialization state or call the llResetScript() function in response.
The first option is far too inconvenient for our users. The second option is a common use case, so we’ll look at the third option.
Since our script resides in the Running state during operation, we’ll catch the changed() event there:
-
state Running
-
{
-
{
-
}
-
-
{
-
{
-
state default;
-
}
-
}
-
}
Because the changed event is fired by user edits to the notecard, our script automatically re-reads their configuration changes and applies them.
Providing Feedback to Users
One last thing you will want to do when reading the configuration notecard is provide feedback to the user so they know their changes were applied. This is best done during the state_exit() event of your initialization state:
-
{
-
}
Conclusion
The basic steps in this article can be used to add user-configurability to your scripts, making it easier for your users to tune settings and make adjustments without having to contact you for adjustments.
If you have any questions please feel free to IM me, Squeebee Wakawaka.
Here is our final script:
-
string CONFIG_CARD = "Configuration";
-
float VOLUME = 0.5;
-
float SPEED = 0.05;
-
-
integer NotecardLine;
-
key QueryID;
-
-
default
-
{
-
{
-
-
{
-
NotecardLine = 0;
-
}
-
{
-
state Running;
-
}
-
}
-
-
{
-
list temp;
-
string name;
-
string value;
-
-
{
-
{
-
{
-
-
{
-
}
-
{
-
}
-
}
-
NotecardLine++;
-
}
-
{
-
state Running;
-
}
-
}
-
}
-
-
{
-
}
-
}
-
-
state Running
-
{
-
{
-
}
-
-
{
-
{
-
state default;
-